BarInsta/app/src/main/java/awais/instagrabber/managers/ThreadManager.kt

1391 lines
54 KiB
Kotlin

package awais.instagrabber.managers
import android.content.ContentResolver
import android.net.Uri
import android.util.Log
import androidx.core.util.Pair
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations.distinctUntilChanged
import androidx.lifecycle.Transformations.map
import awais.instagrabber.R
import awais.instagrabber.customviews.emoji.Emoji
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Resource.Companion.error
import awais.instagrabber.models.Resource.Companion.loading
import awais.instagrabber.models.Resource.Companion.success
import awais.instagrabber.models.enums.DirectItemType
import awais.instagrabber.repositories.requests.UploadFinishOptions
import awais.instagrabber.repositories.requests.VideoOptions
import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds
import awais.instagrabber.repositories.requests.directmessages.ThreadIdsOrUserIds.Companion.of
import awais.instagrabber.repositories.responses.User
import awais.instagrabber.repositories.responses.directmessages.*
import awais.instagrabber.repositories.responses.giphy.GiphyGif
import awais.instagrabber.utils.*
import awais.instagrabber.utils.MediaUploader.MediaUploadResponse
import awais.instagrabber.utils.MediaUploader.uploadPhoto
import awais.instagrabber.utils.MediaUploader.uploadVideo
import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener
import awais.instagrabber.utils.MediaUtils.VideoInfo
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.extensions.TAG
import awais.instagrabber.webservices.DirectMessagesRepository
import awais.instagrabber.webservices.FriendshipRepository
import awais.instagrabber.webservices.MediaRepository
import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterables
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call
import java.io.IOException
import java.net.HttpURLConnection
import java.util.*
import java.util.stream.Collectors
class ThreadManager(
private val threadId: String,
pending: Boolean,
private val currentUser: User?,
private val contentResolver: ContentResolver,
private val viewerId: Long,
private val csrfToken: String,
private val deviceUuid: String,
) {
private val _fetching = MutableLiveData<Resource<Any?>>()
val fetching: LiveData<Resource<Any?>> = _fetching
private val _replyToItem = MutableLiveData<DirectItem?>()
val replyToItem: LiveData<DirectItem?> = _replyToItem
private val _pendingRequests = MutableLiveData<DirectThreadParticipantRequestsResponse?>(null)
val pendingRequests: LiveData<DirectThreadParticipantRequestsResponse?> = _pendingRequests
private val inboxManager: InboxManager = if (pending) DirectMessagesManager.pendingInboxManager else DirectMessagesManager.inboxManager
private val threadIdsOrUserIds: ThreadIdsOrUserIds = of(threadId)
private val friendshipRepository: FriendshipRepository by lazy { FriendshipRepository.getInstance() }
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
private val directMessagesRepository by lazy { DirectMessagesRepository.getInstance() }
val thread: LiveData<DirectThread?> by lazy {
distinctUntilChanged(map(inboxManager.getInbox()) { inboxResource: Resource<DirectInbox?>? ->
if (inboxResource == null) return@map null
val (threads) = inboxResource.data ?: return@map null
if (threads.isNullOrEmpty()) return@map null
val thread = threads.firstOrNull { it.threadId == threadId }
thread?.also {
cursor = thread.oldestCursor
hasOlder = thread.hasOlder
}
})
}
val inputMode: LiveData<Int> by lazy { distinctUntilChanged(map(thread) { it?.inputMode ?: 1 }) }
val threadTitle: LiveData<String?> by lazy { distinctUntilChanged(map(thread) { it?.threadTitle }) }
val users: LiveData<List<User>> by lazy { distinctUntilChanged(map(thread) { it?.users ?: emptyList() }) }
val usersWithCurrent: LiveData<List<User>> by lazy {
distinctUntilChanged(map(thread) {
if (it == null) return@map emptyList()
getUsersWithCurrentUser(it)
})
}
val leftUsers: LiveData<List<User>> by lazy { distinctUntilChanged(map(thread) { it?.leftUsers ?: emptyList() }) }
val usersAndLeftUsers: LiveData<Pair<List<User>, List<User>>> by lazy {
distinctUntilChanged(map(thread) {
if (it == null) return@map Pair<List<User>, List<User>>(emptyList(), emptyList())
val users = getUsersWithCurrentUser(it)
val leftUsers = it.leftUsers
Pair(users, leftUsers)
})
}
val isPending: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.pending ?: true }) }
val adminUserIds: LiveData<List<Long>> by lazy { distinctUntilChanged(map(thread) { it?.adminUserIds ?: emptyList() }) }
val items: LiveData<List<DirectItem>> by lazy { distinctUntilChanged(map(thread) { it?.items ?: emptyList() }) }
val isViewerAdmin: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.adminUserIds?.contains(viewerId) ?: false }) }
val isGroup: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.isGroup ?: false }) }
val isMuted: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.muted ?: false }) }
val isApprovalRequiredToJoin: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.approvalRequiredForNewMembers ?: false }) }
val isMentionsMuted: LiveData<Boolean> by lazy { distinctUntilChanged(map(thread) { it?.mentionsMuted ?: false }) }
val pendingRequestsCount: LiveData<Int> by lazy { distinctUntilChanged(map(_pendingRequests) { it?.totalParticipantRequests ?: 0 }) }
val inviter: LiveData<User?> by lazy { distinctUntilChanged(map(thread) { it?.inviter }) }
private var hasOlder = true
private var cursor: String? = null
private var chatsRequest: Call<DirectThreadFeedResponse?>? = null
private fun getUsersWithCurrentUser(t: DirectThread): List<User> {
val builder = ImmutableList.builder<User>()
if (currentUser != null) {
builder.add(currentUser)
}
val users: List<User>? = t.users
if (users != null) {
builder.addAll(users)
}
return builder.build()
}
fun fetchChats(scope: CoroutineScope) {
val fetchingValue = _fetching.value
if (fetchingValue != null && fetchingValue.status === Resource.Status.LOADING || !hasOlder) return
_fetching.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val threadFeedResponse = directMessagesRepository.fetchThread(threadId, cursor)
if (threadFeedResponse.status != null && threadFeedResponse.status != "ok") {
_fetching.postValue(error(R.string.generic_not_ok_response, null))
return@launch
}
val thread = threadFeedResponse.thread
if (thread == null) {
_fetching.postValue(error("thread is null!", null))
return@launch
}
setThread(thread)
_fetching.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "Failed fetching dm chats", e)
_fetching.postValue(error(e.message, null))
hasOlder = false
}
}
if (cursor == null) {
fetchPendingRequests(scope)
}
}
fun fetchPendingRequests(scope: CoroutineScope) {
val isGroup = isGroup.value
if (isGroup == null || !isGroup) return
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.participantRequests(threadId, 1)
_pendingRequests.postValue(response)
} catch (e: Exception) {
Log.e(TAG, "fetchPendingRequests: ", e)
}
}
}
private fun setThread(thread: DirectThread, skipItems: Boolean) {
// if (thread.getInputMode() != 1 && thread.isGroup() && viewerIsAdmin) {
// fetchPendingRequests();
// }
val items = thread.items
if (skipItems) {
val currentThread = this.thread.value
if (currentThread != null) {
thread.items = currentThread.items
}
}
if (!skipItems && !cursor.isNullOrBlank()) {
val currentThread = this.thread.value
if (currentThread != null) {
val currentItems = currentThread.items
val list = if (currentItems == null) LinkedList() else LinkedList(currentItems)
if (items != null) {
list.addAll(items)
}
thread.items = list
}
}
inboxManager.setThread(threadId, thread)
}
private fun setThread(thread: DirectThread) {
setThread(thread, false)
}
private fun setThreadUsers(users: List<User>?, leftUsers: List<User>?) {
val currentThread = thread.value ?: return
val thread: DirectThread = try {
currentThread.clone() as DirectThread
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "setThreadUsers: ", e)
return
}
if (users != null) {
thread.users = users
}
if (leftUsers != null) {
thread.leftUsers = leftUsers
}
inboxManager.setThread(threadId, thread)
}
private fun addItems(index: Int, items: Collection<DirectItem>) {
inboxManager.addItemsToThread(threadId, index, items)
}
private fun addReaction(item: DirectItem, emoji: Emoji) {
if (currentUser == null) return
val isLike = emoji.unicode == "❤️"
var reactions = item.reactions
reactions = if (reactions == null) {
DirectItemReactions(null, null)
} else {
try {
reactions.clone() as DirectItemReactions
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "addReaction: ", e)
return
}
}
if (isLike) {
val likes = addEmoji(reactions.likes, null, false)
reactions.likes = likes
}
val emojis = addEmoji(reactions.emojis, emoji.unicode, true)
reactions.emojis = emojis
val currentItems = items.value
val items = if (currentItems == null) LinkedList() else LinkedList(currentItems)
val index = getItemIndex(item, items)
if (index >= 0) {
try {
val clone = items[index].clone() as DirectItem
clone.reactions = reactions
items[index] = clone
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "addReaction: error cloning", e)
}
}
inboxManager.setItemsToThread(threadId, items)
}
private fun removeReaction(item: DirectItem) {
try {
val itemClone = item.clone() as DirectItem
val reactions = itemClone.reactions
var reactionsClone: DirectItemReactions? = null
if (reactions != null) {
reactionsClone = reactions.clone() as DirectItemReactions
}
var likes: List<DirectItemEmojiReaction>? = null
if (reactionsClone != null) {
likes = reactionsClone.likes
}
if (likes != null) {
val updatedLikes = likes.stream()
.filter { (senderId) -> senderId != viewerId }
.collect(Collectors.toList())
if (reactionsClone != null) {
reactionsClone.likes = updatedLikes
}
}
var emojis: List<DirectItemEmojiReaction>? = null
if (reactionsClone != null) {
emojis = reactionsClone.emojis
}
if (emojis != null) {
val updatedEmojis = emojis.stream()
.filter { (senderId) -> senderId != viewerId }
.collect(Collectors.toList())
if (reactionsClone != null) {
reactionsClone.emojis = updatedEmojis
}
}
itemClone.reactions = reactionsClone
val items = items.value
val list = if (items == null) LinkedList() else LinkedList(items)
val index = getItemIndex(item, list)
if (index >= 0) {
list[index] = itemClone
}
inboxManager.setItemsToThread(threadId, list)
} catch (e: Exception) {
Log.e(TAG, "removeReaction: ", e)
}
}
private fun removeItem(item: DirectItem): Int {
val items = items.value
val list = if (items == null) LinkedList() else LinkedList(items)
val index = getItemIndex(item, list)
if (index >= 0) {
list.removeAt(index)
inboxManager.setItemsToThread(threadId, list)
}
return index
}
private fun addEmoji(
reactionList: List<DirectItemEmojiReaction>?,
emoji: String?,
shouldReplaceIfAlreadyReacted: Boolean,
): List<DirectItemEmojiReaction>? {
if (currentUser == null) return reactionList
val temp: MutableList<DirectItemEmojiReaction> = if (reactionList == null) ArrayList() else ArrayList(reactionList)
var index = -1
for (i in temp.indices) {
val (senderId) = temp[i]
if (senderId == currentUser.pk) {
index = i
break
}
}
val reaction = DirectItemEmojiReaction(
currentUser.pk,
System.currentTimeMillis() * 1000,
emoji,
"none"
)
if (index < 0) {
temp.add(0, reaction)
} else if (shouldReplaceIfAlreadyReacted) {
temp[index] = reaction
}
return temp
}
fun sendText(text: String, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
val userId = getCurrentUserId(data) ?: return data
val clientContext = UUID.randomUUID().toString()
val replyToItemValue = _replyToItem.value
val directItem = createText(userId, clientContext, text, replyToItemValue)
// Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId());
directItem.isPending = true
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
val repliedToItemId = replyToItemValue?.itemId
val repliedToClientContext = replyToItemValue?.clientContext
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.broadcastText(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdsOrUserIds,
text,
repliedToItemId,
repliedToClientContext
)
parseResponse(response, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, directItem))
Log.e(TAG, "sendText: ", e)
}
}
return data
}
fun sendUri(uri: Uri, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
val mimeType = Utils.getMimeType(uri, contentResolver)
if (isEmpty(mimeType)) {
data.postValue(error("Unknown MediaType", null))
return data
}
val isPhoto = mimeType != null && mimeType.startsWith("image")
if (isPhoto) {
sendPhoto(data, uri, scope)
return data
}
if (mimeType != null && mimeType.startsWith("video")) {
sendVideo(data, uri, scope)
}
return data
}
fun sendAnimatedMedia(giphyGif: GiphyGif, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
val userId = getCurrentUserId(data) ?: return data
val clientContext = UUID.randomUUID().toString()
val directItem = createAnimatedMedia(userId, clientContext, giphyGif)
directItem.isPending = true
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
scope.launch(Dispatchers.IO) {
try {
val request = directMessagesRepository.broadcastAnimatedMedia(
csrfToken,
userId,
deviceUuid,
clientContext,
threadIdsOrUserIds,
giphyGif
)
parseResponse(request, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, directItem))
Log.e(TAG, "sendAnimatedMedia: ", e)
}
}
return data
}
fun sendVoice(
data: MutableLiveData<Resource<Any?>>,
uri: Uri,
waveform: List<Float>,
samplingFreq: Int,
duration: Long,
byteLength: Long,
scope: CoroutineScope,
) {
if (duration > 60000) {
// instagram does not allow uploading audio longer than 60 secs for Direct messages
data.postValue(error(R.string.dms_ERROR_AUDIO_TOO_LONG, null))
return
}
val userId = getCurrentUserId(data) ?: return
val clientContext = UUID.randomUUID().toString()
val directItem = createVoice(userId, clientContext, uri, duration, waveform, samplingFreq)
directItem.isPending = true
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
val uploadDmVoiceOptions = createUploadDmVoiceOptions(byteLength, duration)
scope.launch(Dispatchers.IO) {
try {
val response = uploadVideo(uri, contentResolver, uploadDmVoiceOptions)
// Log.d(TAG, "onUploadComplete: " + response);
if (handleInvalidResponse(data, response)) return@launch
val uploadFinishOptions = UploadFinishOptions(
uploadDmVoiceOptions.uploadId,
"4",
null
)
mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = directMessagesRepository.broadcastVoice(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdsOrUserIds,
uploadDmVoiceOptions.uploadId,
waveform,
samplingFreq
)
parseResponse(broadcastResponse, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, directItem))
Log.e(TAG, "sendVoice: ", e)
}
}
}
fun sendReaction(
item: DirectItem,
emoji: Emoji,
scope: CoroutineScope,
): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
val userId = getCurrentUserId(data)
if (userId == null) {
data.postValue(error("userId is null", null))
return data
}
val clientContext = UUID.randomUUID().toString()
// Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId());
data.postValue(loading(item))
addReaction(item, emoji)
var emojiUnicode: String? = null
if (emoji.unicode != "❤️") {
emojiUnicode = emoji.unicode
}
val itemId = item.itemId
if (itemId == null) {
data.postValue(error("itemId is null", null))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.broadcastReaction(
csrfToken,
userId,
deviceUuid,
clientContext,
threadIdsOrUserIds,
itemId,
emojiUnicode,
false
)
} catch (e: Exception) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendReaction: ", e)
}
}
return data
}
fun sendDeleteReaction(itemId: String, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
val item = getItem(itemId)
if (item == null) {
data.postValue(error("Invalid item", null))
return data
}
val reactions = item.reactions
if (reactions == null) {
// already removed?
data.postValue(success(item))
return data
}
removeReaction(item)
val clientContext = UUID.randomUUID().toString()
val itemId1 = item.itemId
if (itemId1 == null) {
data.postValue(error("itemId is null", null))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.broadcastReaction(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdsOrUserIds,
itemId1,
null,
true
)
} catch (e: Exception) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendDeleteReaction: ", e)
}
}
return data
}
fun unsend(item: DirectItem, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
val index = removeItem(item)
val itemId = item.itemId
if (itemId == null) {
data.postValue(error("itemId is null", null))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.deleteItem(csrfToken, deviceUuid, threadId, itemId)
} catch (e: Exception) {
// add the item back if unsuccessful
addItems(index, listOf(item))
data.postValue(error(e.message, item))
Log.e(TAG, "unsend: ", e)
}
}
return data
}
fun forward(
recipients: Set<RankedRecipient>,
itemToForward: DirectItem,
scope: CoroutineScope,
) {
for (recipient in recipients) {
forward(recipient, itemToForward, scope)
}
}
fun forward(
recipient: RankedRecipient,
itemToForward: DirectItem,
scope: CoroutineScope,
) {
if (recipient.thread == null && recipient.user != null) {
scope.launch(Dispatchers.IO) {
// create thread and forward
val thread = DirectMessagesManager.createThread(recipient.user.pk)
forward(thread, itemToForward, scope)
}
return
}
if (recipient.thread != null) {
// just forward
val thread = recipient.thread
forward(thread, itemToForward, scope)
}
}
fun setReplyToItem(item: DirectItem?) {
// Log.d(TAG, "setReplyToItem: " + item);
_replyToItem.postValue(item)
}
private fun forward(
thread: DirectThread,
itemToForward: DirectItem,
scope: CoroutineScope,
): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
val forwardItemId = itemToForward.itemId
if (forwardItemId == null) {
data.postValue(error("item id is null", null))
return data
}
val itemType = itemToForward.itemType
if (itemType == null) {
data.postValue(error("item type is null", null))
return data
}
val itemTypeName = DirectItemType.getName(itemType)
if (itemTypeName == null) {
Log.e(TAG, "forward: itemTypeName was null!")
data.postValue(error("itemTypeName is null", null))
return data
}
data.postValue(loading(null))
if (thread.threadId == null) {
Log.e(TAG, "forward: threadId was null!")
data.postValue(error("threadId is null", null))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.forward(
thread.threadId,
itemTypeName,
threadId,
forwardItemId
)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "forward: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun acceptRequest(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.approveRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "acceptRequest: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun declineRequest(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.declineRequest(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "declineRequest: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun refreshChats(scope: CoroutineScope) {
val isFetching = _fetching.value
if (isFetching != null && isFetching.status === Resource.Status.LOADING) {
stopCurrentRequest()
}
cursor = null
hasOlder = true
fetchChats(scope)
}
private fun sendPhoto(
data: MutableLiveData<Resource<Any?>>,
uri: Uri,
scope: CoroutineScope,
) {
try {
val dimensions = BitmapUtils.decodeDimensions(contentResolver, uri)
if (dimensions == null) {
data.postValue(error("Decoding dimensions failed", null))
return
}
sendPhoto(data, uri, dimensions.first, dimensions.second, scope)
} catch (e: IOException) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendPhoto: ", e)
}
}
private fun sendPhoto(
data: MutableLiveData<Resource<Any?>>,
uri: Uri,
width: Int,
height: Int,
scope: CoroutineScope,
) {
val clientContext = UUID.randomUUID().toString()
val directItem = createImageOrVideo(viewerId, clientContext, uri, width, height, false)
directItem.isPending = true
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
scope.launch(Dispatchers.IO) {
try {
val response = uploadPhoto(uri, contentResolver)
if (handleInvalidResponse(data, response)) return@launch
val response1 = response.response ?: return@launch
val uploadId = response1.optString("upload_id")
val response2 = directMessagesRepository.broadcastPhoto(csrfToken, viewerId, deviceUuid, clientContext, threadIdsOrUserIds, uploadId)
parseResponse(response2, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, null))
Log.e(TAG, "sendPhoto: ", e)
}
}
}
private fun sendVideo(
data: MutableLiveData<Resource<Any?>>,
uri: Uri,
scope: CoroutineScope,
) {
MediaUtils.getVideoInfo(contentResolver, uri, object : OnInfoLoadListener<VideoInfo?> {
override fun onLoad(info: VideoInfo?) {
if (info == null) {
data.postValue(error("Could not get the video info", null))
return
}
sendVideo(data, uri, info.size, info.duration, info.width, info.height, scope)
}
override fun onFailure(t: Throwable) {
data.postValue(error(t.message, null))
}
})
}
private fun sendVideo(
data: MutableLiveData<Resource<Any?>>,
uri: Uri,
byteLength: Long,
duration: Long,
width: Int,
height: Int,
scope: CoroutineScope,
) {
if (duration > 60000) {
// instagram does not allow uploading videos longer than 60 secs for Direct messages
data.postValue(error(R.string.dms_ERROR_VIDEO_TOO_LONG, null))
return
}
val userId = getCurrentUserId(data) ?: return
val clientContext = UUID.randomUUID().toString()
val directItem = createImageOrVideo(userId, clientContext, uri, width, height, true)
directItem.isPending = true
addItems(0, listOf(directItem))
data.postValue(loading(directItem))
val uploadDmVideoOptions = createUploadDmVideoOptions(byteLength, duration, width, height)
scope.launch(Dispatchers.IO) {
try {
val response = uploadVideo(uri, contentResolver, uploadDmVideoOptions)
// Log.d(TAG, "onUploadComplete: " + response);
if (handleInvalidResponse(data, response)) return@launch
val uploadFinishOptions = UploadFinishOptions(
uploadDmVideoOptions.uploadId,
"2",
VideoOptions(duration / 1000f, emptyList(), 0, false)
)
mediaRepository.uploadFinish(csrfToken, userId, deviceUuid, uploadFinishOptions)
val broadcastResponse = directMessagesRepository.broadcastVideo(
csrfToken,
viewerId,
deviceUuid,
clientContext,
threadIdsOrUserIds,
uploadDmVideoOptions.uploadId,
"",
true
)
parseResponse(broadcastResponse, data, directItem)
} catch (e: Exception) {
data.postValue(error(e.message, directItem))
Log.e(TAG, "sendVideo: ", e)
}
}
}
private fun parseResponse(
response: DirectThreadBroadcastResponse,
data: MutableLiveData<Resource<Any?>>,
directItem: DirectItem,
) {
val payloadClientContext: String?
val timestamp: Long
val itemId: String?
val payload = response.payload
if (payload == null) {
val messageMetadata = response.messageMetadata
if (messageMetadata == null || messageMetadata.isEmpty()) {
data.postValue(success(directItem))
return
}
val (clientContext, itemId1, timestamp1) = messageMetadata[0]
payloadClientContext = clientContext
itemId = itemId1
timestamp = timestamp1
} else {
payloadClientContext = payload.clientContext
timestamp = payload.timestamp
itemId = payload.itemId
}
if (payloadClientContext == null) {
data.postValue(error("clientContext in response was null", null))
return
}
updateItemSent(payloadClientContext, timestamp, itemId)
data.postValue(success(directItem))
}
private fun updateItemSent(
clientContext: String,
timestamp: Long,
itemId: String?,
) {
val items = items.value
val list = if (items == null) LinkedList() else LinkedList(items)
val index = list.indexOfFirst { it?.clientContext == clientContext }
if (index < 0) return
val directItem = list[index]
try {
val itemClone = directItem.clone() as DirectItem
itemClone.itemId = itemId
itemClone.isPending = false
itemClone.setTimestamp(timestamp)
list[index] = itemClone
inboxManager.setItemsToThread(threadId, list)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "updateItemSent: ", e)
}
}
private fun handleInvalidResponse(
data: MutableLiveData<Resource<Any?>>,
response: MediaUploadResponse,
): Boolean {
val responseJson = response.response
if (responseJson == null || response.responseCode != HttpURLConnection.HTTP_OK) {
data.postValue(error(R.string.generic_not_ok_response, null))
return true
}
val status = responseJson.optString("status")
if (isEmpty(status) || status != "ok") {
data.postValue(error(R.string.generic_not_ok_response, null))
return true
}
return false
}
private fun getItemIndex(item: DirectItem, list: List<DirectItem?>): Int {
return Iterables.indexOf(list) { i: DirectItem? -> i != null && i.itemId == item.itemId }
}
private fun getItem(itemId: String): DirectItem? {
val items = items.value ?: return null
return items.asSequence()
.filter { it.itemId == itemId }
.firstOrNull()
}
private fun stopCurrentRequest() {
chatsRequest?.let {
if (it.isExecuted || it.isCanceled) return
it.cancel()
}
_fetching.postValue(success(Any()))
}
private fun getCurrentUserId(data: MutableLiveData<Resource<Any?>>): Long? {
if (currentUser == null || currentUser.pk <= 0) {
data.postValue(error(R.string.dms_ERROR_INVALID_USER, null))
return null
}
return currentUser.pk
}
fun removeThread() {
val pendingValue = isPending.value
val threadInPending = pendingValue != null && pendingValue
inboxManager.removeThread(threadId)
if (threadInPending) {
val totalValue = inboxManager.getPendingRequestsTotal().value ?: return
inboxManager.setPendingRequestsTotal(totalValue - 1)
}
}
fun updateTitle(newTitle: String, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.updateTitle(csrfToken, deviceUuid, threadId, newTitle.trim())
handleDetailsChangeResponse(data, response)
} catch (e: Exception) {
}
}
return data
}
fun addMembers(users: Set<User>, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.addUsers(
csrfToken,
deviceUuid,
threadId,
users.map { obj: User -> obj.pk }
)
handleDetailsChangeResponse(data, response)
} catch (e: Exception) {
Log.e(TAG, "addMembers: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun removeMember(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.removeUsers(csrfToken, deviceUuid, threadId, setOf(user.pk))
data.postValue(success(Any()))
var activeUsers = users.value
var leftUsersValue = leftUsers.value
if (activeUsers == null) {
activeUsers = emptyList()
}
if (leftUsersValue == null) {
leftUsersValue = emptyList()
}
val updatedActiveUsers = activeUsers.filter { u: User -> u.pk != user.pk }
val updatedLeftUsersBuilder = ImmutableList.builder<User>().addAll(leftUsersValue)
if (!leftUsersValue.contains(user)) {
updatedLeftUsersBuilder.add(user)
}
val updatedLeftUsers = updatedLeftUsersBuilder.build()
setThreadUsers(updatedActiveUsers, updatedLeftUsers)
} catch (e: Exception) {
Log.e(TAG, "removeMember: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun isAdmin(user: User): Boolean {
val adminUserIdsValue = adminUserIds.value
return adminUserIdsValue != null && adminUserIdsValue.contains(user.pk)
}
fun makeAdmin(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
if (isAdmin(user)) return data
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.addAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdminIds = adminUserIds.value
val updatedAdminIds = ImmutableList.builder<Long>()
.addAll(currentAdminIds ?: emptyList())
.add(user.pk)
.build()
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.adminUserIds = updatedAdminIds
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "makeAdmin: ", e)
}
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "makeAdmin: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun removeAdmin(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
if (!isAdmin(user)) return data
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.removeAdmins(csrfToken, deviceUuid, threadId, setOf(user.pk))
val currentAdmins = adminUserIds.value ?: return@launch
val updatedAdminUserIds = currentAdmins.filter { userId1: Long -> userId1 != user.pk }
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.adminUserIds = updatedAdminUserIds
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "removeAdmin: ", e)
}
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "removeAdmin: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun mute(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
val muted = isMuted.value
if (muted != null && muted) {
data.postValue(success(Any()))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.mute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.muted = true
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "mute: ", e)
}
} catch (e: Exception) {
Log.e(TAG, "mute: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun unmute(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
val muted = isMuted.value
if (muted != null && !muted) {
data.postValue(success(Any()))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.unmute(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.muted = false
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "unmute: ", e)
}
} catch (e: Exception) {
Log.e(TAG, "unmute: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun muteMentions(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
val mentionsMuted = isMentionsMuted.value
if (mentionsMuted != null && mentionsMuted) {
data.postValue(success(Any()))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.muteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.mentionsMuted = true
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "muteMentions: ", e)
}
} catch (e: Exception) {
Log.e(TAG, "muteMentions: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun unmuteMentions(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
val mentionsMuted = isMentionsMuted.value
if (mentionsMuted != null && !mentionsMuted) {
data.postValue(success(Any()))
return data
}
scope.launch(Dispatchers.IO) {
try {
directMessagesRepository.unmuteMentions(csrfToken, deviceUuid, threadId)
data.postValue(success(Any()))
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.mentionsMuted = false
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "unmuteMentions: ", e)
}
} catch (e: Exception) {
Log.e(TAG, "unmuteMentions: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun blockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
friendshipRepository.changeBlock(csrfToken, viewerId, deviceUuid, false, user.pk)
refreshChats(scope)
} catch (e: Exception) {
Log.e(TAG, "onFailure: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun unblockUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
friendshipRepository.changeBlock(csrfToken, viewerId, deviceUuid, true, user.pk)
refreshChats(scope)
} catch (e: Exception) {
Log.e(TAG, "onFailure: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun restrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
friendshipRepository.toggleRestrict(csrfToken, deviceUuid, user.pk, true)
refreshChats(scope)
} catch (e: Exception) {
Log.e(TAG, "onFailure: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun unRestrictUser(user: User, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
scope.launch(Dispatchers.IO) {
try {
friendshipRepository.toggleRestrict(csrfToken, deviceUuid, user.pk, false)
refreshChats(scope)
} catch (e: Exception) {
Log.e(TAG, "onFailure: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun approveUsers(users: List<User>, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.approveParticipantRequests(
csrfToken,
deviceUuid,
threadId,
users.map { obj: User -> obj.pk }
)
handleDetailsChangeResponse(data, response)
pendingUserApproveDenySuccessAction(users)
} catch (e: Exception) {
Log.e(TAG, "approveUsers: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun denyUsers(users: List<User>, scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.declineParticipantRequests(
csrfToken,
deviceUuid,
threadId,
users.map { obj: User -> obj.pk }
)
handleDetailsChangeResponse(data, response)
pendingUserApproveDenySuccessAction(users)
} catch (e: Exception) {
Log.e(TAG, "denyUsers: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
private fun pendingUserApproveDenySuccessAction(users: List<User>) {
val pendingRequestsValue = _pendingRequests.value ?: return
val pendingUsers = pendingRequestsValue.users
if (pendingUsers == null || pendingUsers.isEmpty()) return
val filtered = pendingUsers.filter { o: User -> !users.contains(o) }
try {
val clone = pendingRequestsValue.clone() as DirectThreadParticipantRequestsResponse
clone.users = filtered
val totalParticipantRequests = clone.totalParticipantRequests
clone.totalParticipantRequests = if (totalParticipantRequests > 0) totalParticipantRequests - 1 else 0
_pendingRequests.postValue(clone)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "pendingUserApproveDenySuccessAction: ", e)
}
}
fun approvalRequired(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
val approvalRequiredToJoin = isApprovalRequiredToJoin.value
if (approvalRequiredToJoin != null && approvalRequiredToJoin) {
data.postValue(success(Any()))
return data
}
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.approvalRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, response)
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.approvalRequiredForNewMembers = true
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "onResponse: ", e)
}
} catch (e: Exception) {
Log.e(TAG, "approvalRequired: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun approvalNotRequired(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
val approvalRequiredToJoin = isApprovalRequiredToJoin.value
if (approvalRequiredToJoin != null && !approvalRequiredToJoin) {
data.postValue(success(Any()))
return data
}
scope.launch(Dispatchers.IO) {
try {
val request = directMessagesRepository.approvalNotRequired(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.approvalRequiredForNewMembers = false
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "onResponse: ", e)
}
} catch (e: Exception) {
Log.e(TAG, "approvalNotRequired: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun leave(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val request = directMessagesRepository.leave(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
} catch (e: Exception) {
Log.e(TAG, "leave: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
fun end(scope: CoroutineScope): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val request = directMessagesRepository.end(csrfToken, deviceUuid, threadId)
handleDetailsChangeResponse(data, request)
val currentThread = thread.value ?: return@launch
try {
val thread = currentThread.clone() as DirectThread
thread.inputMode = 1
inboxManager.setThread(threadId, thread)
} catch (e: CloneNotSupportedException) {
Log.e(TAG, "onResponse: ", e)
}
} catch (e: Exception) {
Log.e(TAG, "leave: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
private fun handleDetailsChangeResponse(
data: MutableLiveData<Resource<Any?>>,
response: DirectThreadDetailsChangeResponse,
) {
data.postValue(success(Any()))
val thread = response.thread
if (thread != null) {
setThread(thread, true)
}
}
fun markAsSeen(
directItem: DirectItem,
scope: CoroutineScope,
): LiveData<Resource<Any?>> {
val data = MutableLiveData<Resource<Any?>>()
data.postValue(loading(null))
scope.launch(Dispatchers.IO) {
try {
val response = directMessagesRepository.markAsSeen(csrfToken, deviceUuid, threadId, directItem)
if (response == null) {
data.postValue(error(R.string.generic_null_response, null))
return@launch
}
if (currentUser == null) return@launch
inboxManager.fetchUnseenCount(scope)
val payload = response.payload ?: return@launch
val timestamp = payload.timestamp
val thread = thread.value ?: return@launch
val currentLastSeenAt = thread.lastSeenAt
val lastSeenAt = if (currentLastSeenAt == null) HashMap() else HashMap(currentLastSeenAt)
lastSeenAt[currentUser.pk] = DirectThreadLastSeenAt(timestamp, directItem.itemId)
thread.lastSeenAt = lastSeenAt
setThread(thread, true)
data.postValue(success(Any()))
} catch (e: Exception) {
Log.e(TAG, "markAsSeen: ", e)
data.postValue(error(e.message, null))
}
}
return data
}
}