mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-20 05:58:27 +00:00
refactor: use OpenGroupAPI.kt instead of PublicChatAPI.kt for all open group operations.
Syncing open group notify conversation list listeners debounced so UI doesn't freeze on initial syncs
This commit is contained in:
parent
b6769ffddc
commit
7121aa85fb
@ -21,9 +21,9 @@ import android.content.Intent;
|
|||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||||
@ -33,6 +33,7 @@ import org.conscrypt.Conscrypt;
|
|||||||
import org.session.libsession.messaging.MessagingConfiguration;
|
import org.session.libsession.messaging.MessagingConfiguration;
|
||||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
import org.session.libsession.messaging.avatars.AvatarHelper;
|
||||||
import org.session.libsession.messaging.jobs.JobQueue;
|
import org.session.libsession.messaging.jobs.JobQueue;
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller;
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller;
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||||
@ -50,14 +51,12 @@ import org.session.libsignal.service.loki.api.PushNotificationAPI;
|
|||||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||||
import org.session.libsignal.service.loki.api.SwarmAPI;
|
import org.session.libsignal.service.loki.api.SwarmAPI;
|
||||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
||||||
import org.thoughtcrime.securesms.jobmanager.DependencyInjector;
|
import org.thoughtcrime.securesms.jobmanager.DependencyInjector;
|
||||||
@ -140,10 +139,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
public Poller poller = null;
|
public Poller poller = null;
|
||||||
public ClosedGroupPoller closedGroupPoller = null;
|
public ClosedGroupPoller closedGroupPoller = null;
|
||||||
public PublicChatManager publicChatManager = null;
|
public PublicChatManager publicChatManager = null;
|
||||||
private PublicChatAPI publicChatAPI = null;
|
|
||||||
public Broadcaster broadcaster = null;
|
public Broadcaster broadcaster = null;
|
||||||
public SignalCommunicationModule communicationModule;
|
public SignalCommunicationModule communicationModule;
|
||||||
private Job firebaseInstanceIdJob;
|
private Job firebaseInstanceIdJob;
|
||||||
|
private Handler threadNotificationHandler;
|
||||||
|
|
||||||
private volatile boolean isAppVisible;
|
private volatile boolean isAppVisible;
|
||||||
|
|
||||||
@ -151,7 +150,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
return (ApplicationContext) context.getApplicationContext();
|
return (ApplicationContext) context.getApplicationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public Handler getThreadNotificationHandler() {
|
||||||
|
return this.threadNotificationHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
@ -166,6 +169,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
// ========
|
// ========
|
||||||
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
||||||
broadcaster = new Broadcaster(this);
|
broadcaster = new Broadcaster(this);
|
||||||
|
threadNotificationHandler = new Handler(Looper.getMainLooper());
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||||
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
|
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
|
||||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
||||||
@ -285,22 +289,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loki
|
// Loki
|
||||||
public @Nullable
|
|
||||||
PublicChatAPI getPublicChatAPI() {
|
|
||||||
if (publicChatAPI != null || !IdentityKeyUtil.hasIdentityKey(this)) {
|
|
||||||
return publicChatAPI;
|
|
||||||
}
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey == null) {
|
|
||||||
return publicChatAPI;
|
|
||||||
}
|
|
||||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
|
||||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
|
||||||
GroupDatabase groupDB = DatabaseFactory.getGroupDatabase(this);
|
|
||||||
publicChatAPI = new PublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB, groupDB);
|
|
||||||
return publicChatAPI;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeSecurityProvider() {
|
private void initializeSecurityProvider() {
|
||||||
try {
|
try {
|
||||||
@ -531,21 +519,12 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
|
|
||||||
public void updateOpenGroupProfilePicturesIfNeeded() {
|
public void updateOpenGroupProfilePicturesIfNeeded() {
|
||||||
AsyncTask.execute(() -> {
|
AsyncTask.execute(() -> {
|
||||||
PublicChatAPI publicChatAPI = null;
|
|
||||||
try {
|
|
||||||
publicChatAPI = getPublicChatAPI();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
if (publicChatAPI == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
||||||
String url = TextSecurePreferences.getProfilePictureURL(this);
|
String url = TextSecurePreferences.getProfilePictureURL(this);
|
||||||
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
||||||
for (String server : servers) {
|
for (String server : servers) {
|
||||||
if (profileKey != null) {
|
if (profileKey != null) {
|
||||||
publicChatAPI.setProfilePicture(server, profileKey, url);
|
OpenGroupAPI.setProfilePicture(server, profileKey, url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -16,11 +16,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.Debouncer;
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -31,10 +35,13 @@ public abstract class Database {
|
|||||||
|
|
||||||
protected SQLCipherOpenHelper databaseHelper;
|
protected SQLCipherOpenHelper databaseHelper;
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
|
private final Debouncer threadNotificationDebouncer;
|
||||||
|
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.databaseHelper = databaseHelper;
|
this.databaseHelper = databaseHelper;
|
||||||
|
this.threadNotificationDebouncer = new Debouncer(ApplicationContext.getInstance(context).getThreadNotificationHandler(), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyConversationListeners(Set<Long> threadIds) {
|
protected void notifyConversationListeners(Set<Long> threadIds) {
|
||||||
@ -47,7 +54,7 @@ public abstract class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyConversationListListeners() {
|
protected void notifyConversationListListeners() {
|
||||||
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
|
threadNotificationDebouncer.publish(()->context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyStickerListeners() {
|
protected void notifyStickerListeners() {
|
||||||
|
@ -26,11 +26,12 @@ import kotlinx.coroutines.flow.*
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.utilities.*
|
import org.session.libsession.utilities.*
|
||||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
|
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
|
||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
@ -359,8 +360,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
||||||
|
|
||||||
ApplicationContext.getInstance(context).publicChatAPI!!
|
OpenGroupAPI.leave(publicChat.channel, publicChat.server)
|
||||||
.leave(publicChat.channel, publicChat.server)
|
|
||||||
|
|
||||||
ApplicationContext.getInstance(context).publicChatManager
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
.removeChat(publicChat.server, publicChat.channel)
|
.removeChat(publicChat.server, publicChat.channel)
|
||||||
|
@ -28,6 +28,7 @@ import nl.komponents.kovenant.functional.bind
|
|||||||
import nl.komponents.kovenant.task
|
import nl.komponents.kovenant.task
|
||||||
import nl.komponents.kovenant.ui.alwaysUi
|
import nl.komponents.kovenant.ui.alwaysUi
|
||||||
import org.session.libsession.messaging.avatars.AvatarHelper
|
import org.session.libsession.messaging.avatars.AvatarHelper
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
import org.session.libsession.messaging.threads.Address
|
import org.session.libsession.messaging.threads.Address
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
@ -179,11 +180,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val promises = mutableListOf<Promise<*, Exception>>()
|
val promises = mutableListOf<Promise<*, Exception>>()
|
||||||
val displayName = displayNameToBeUploaded
|
val displayName = displayNameToBeUploaded
|
||||||
if (displayName != null) {
|
if (displayName != null) {
|
||||||
val publicChatAPI = ApplicationContext.getInstance(this).publicChatAPI
|
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
||||||
if (publicChatAPI != null) {
|
promises.addAll(servers.map { OpenGroupAPI.setDisplayName(displayName, it) })
|
||||||
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
|
||||||
promises.addAll(servers.map { publicChatAPI.setDisplayName(displayName, it) })
|
|
||||||
}
|
|
||||||
TextSecurePreferences.setProfileName(this, displayName)
|
TextSecurePreferences.setProfileName(this, displayName)
|
||||||
}
|
}
|
||||||
val profilePicture = profilePictureToBeUploaded
|
val profilePicture = profilePictureToBeUploaded
|
||||||
|
@ -7,12 +7,12 @@ import android.text.TextUtils
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroupInfo
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatInfo
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
@ -65,27 +65,23 @@ class PublicChatManager(private val context: Context) {
|
|||||||
//TODO Declare a specific type of checked exception instead of "Exception".
|
//TODO Declare a specific type of checked exception instead of "Exception".
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(java.lang.Exception::class)
|
@Throws(java.lang.Exception::class)
|
||||||
public fun addChat(server: String, channel: Long): PublicChat {
|
public fun addChat(server: String, channel: Long): OpenGroup {
|
||||||
val groupChatAPI = ApplicationContext.getInstance(context).publicChatAPI
|
|
||||||
?: throw IllegalStateException("LokiPublicChatAPI is not set!")
|
|
||||||
|
|
||||||
// Ensure the auth token is acquired.
|
// Ensure the auth token is acquired.
|
||||||
groupChatAPI.getAuthToken(server).get()
|
OpenGroupAPI.getAuthToken(server).get()
|
||||||
|
|
||||||
val channelInfo = groupChatAPI.getChannelInfo(channel, server).get()
|
val channelInfo = OpenGroupAPI.getChannelInfo(channel, server).get()
|
||||||
return addChat(server, channel, channelInfo)
|
return addChat(server, channel, channelInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public fun addChat(server: String, channel: Long, info: PublicChatInfo): PublicChat {
|
public fun addChat(server: String, channel: Long, info: OpenGroupInfo): OpenGroup {
|
||||||
val chat = PublicChat(channel, server, info.displayName, true)
|
val chat = PublicChat(channel, server, info.displayName, true)
|
||||||
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
||||||
var profilePicture: Bitmap? = null
|
var profilePicture: Bitmap? = null
|
||||||
// Create the group if we don't have one
|
// Create the group if we don't have one
|
||||||
if (threadID < 0) {
|
if (threadID < 0) {
|
||||||
if (info.profilePictureURL.isNotEmpty()) {
|
if (info.profilePictureURL.isNotEmpty()) {
|
||||||
val profilePictureAsByteArray = ApplicationContext.getInstance(context).publicChatAPI
|
val profilePictureAsByteArray = OpenGroupAPI.downloadOpenGroupProfilePicture(server, info.profilePictureURL)
|
||||||
?.downloadOpenGroupProfilePicture(server, info.profilePictureURL)
|
|
||||||
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
|
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
|
||||||
}
|
}
|
||||||
val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, chat.displayName)
|
val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, chat.displayName)
|
||||||
@ -95,12 +91,12 @@ class PublicChatManager(private val context: Context) {
|
|||||||
// Set our name on the server
|
// Set our name on the server
|
||||||
val displayName = TextSecurePreferences.getProfileName(context)
|
val displayName = TextSecurePreferences.getProfileName(context)
|
||||||
if (!TextUtils.isEmpty(displayName)) {
|
if (!TextUtils.isEmpty(displayName)) {
|
||||||
ApplicationContext.getInstance(context).publicChatAPI?.setDisplayName(displayName, server)
|
OpenGroupAPI.setDisplayName(displayName, server)
|
||||||
}
|
}
|
||||||
// Start polling
|
// Start polling
|
||||||
Util.runOnMain { startPollersIfNeeded() }
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
|
|
||||||
return chat
|
return OpenGroup.from(chat)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun removeChat(server: String, channel: Long) {
|
public fun removeChat(server: String, channel: Long) {
|
||||||
|
@ -1,238 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.loki.api
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import org.session.libsession.messaging.threads.Address
|
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceContent
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
|
||||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
|
||||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob
|
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class PublicChatPoller(private val context: Context, private val group: PublicChat) {
|
|
||||||
private val handler by lazy { Handler() }
|
|
||||||
private var hasStarted = false
|
|
||||||
private var isPollOngoing = false
|
|
||||||
public var isCaughtUp = false
|
|
||||||
|
|
||||||
// region Convenience
|
|
||||||
private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
|
||||||
private var displayNameUpdatees = setOf<String>()
|
|
||||||
|
|
||||||
private val api: PublicChatAPI
|
|
||||||
get() = {
|
|
||||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
|
||||||
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
|
|
||||||
val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context)
|
|
||||||
val openGroupDatabase = DatabaseFactory.getGroupDatabase(context)
|
|
||||||
PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase, openGroupDatabase)
|
|
||||||
}()
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Tasks
|
|
||||||
private val pollForNewMessagesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForNewMessages()
|
|
||||||
handler.postDelayed(this, pollForNewMessagesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForDeletedMessagesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForDeletedMessages()
|
|
||||||
handler.postDelayed(this, pollForDeletedMessagesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForModeratorsTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForModerators()
|
|
||||||
handler.postDelayed(this, pollForModeratorsInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForDisplayNamesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForDisplayNames()
|
|
||||||
handler.postDelayed(this, pollForDisplayNamesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
companion object {
|
|
||||||
private val pollForNewMessagesInterval: Long = 4 * 1000
|
|
||||||
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
|
||||||
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
|
||||||
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
fun startIfNeeded() {
|
|
||||||
if (hasStarted) return
|
|
||||||
pollForNewMessagesTask.run()
|
|
||||||
pollForDeletedMessagesTask.run()
|
|
||||||
pollForModeratorsTask.run()
|
|
||||||
pollForDisplayNamesTask.run()
|
|
||||||
hasStarted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
handler.removeCallbacks(pollForNewMessagesTask)
|
|
||||||
handler.removeCallbacks(pollForDeletedMessagesTask)
|
|
||||||
handler.removeCallbacks(pollForModeratorsTask)
|
|
||||||
handler.removeCallbacks(pollForDisplayNamesTask)
|
|
||||||
hasStarted = false
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Polling
|
|
||||||
private fun getDataMessage(message: PublicChatMessage): SignalServiceDataMessage {
|
|
||||||
val id = group.id.toByteArray()
|
|
||||||
val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null)
|
|
||||||
val quote = if (message.quote != null) {
|
|
||||||
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteePublicKey), message.quote!!.quotedMessageBody, listOf())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val attachments = message.attachments.mapNotNull { attachment ->
|
|
||||||
if (attachment.kind != PublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
|
||||||
SignalServiceAttachmentPointer(
|
|
||||||
attachment.serverID,
|
|
||||||
attachment.contentType,
|
|
||||||
ByteArray(0),
|
|
||||||
Optional.of(attachment.size),
|
|
||||||
Optional.absent(),
|
|
||||||
attachment.width, attachment.height,
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.of(attachment.fileName),
|
|
||||||
false,
|
|
||||||
Optional.fromNullable(attachment.caption),
|
|
||||||
attachment.url)
|
|
||||||
}
|
|
||||||
val linkPreview = message.attachments.firstOrNull { it.kind == PublicChatMessage.Attachment.Kind.LinkPreview }
|
|
||||||
val signalLinkPreviews = mutableListOf<SignalServiceDataMessage.Preview>()
|
|
||||||
if (linkPreview != null) {
|
|
||||||
val attachment = SignalServiceAttachmentPointer(
|
|
||||||
linkPreview.serverID,
|
|
||||||
linkPreview.contentType,
|
|
||||||
ByteArray(0),
|
|
||||||
Optional.of(linkPreview.size),
|
|
||||||
Optional.absent(),
|
|
||||||
linkPreview.width, linkPreview.height,
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.of(linkPreview.fileName),
|
|
||||||
false,
|
|
||||||
Optional.fromNullable(linkPreview.caption),
|
|
||||||
linkPreview.url)
|
|
||||||
signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment)))
|
|
||||||
}
|
|
||||||
val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body
|
|
||||||
val syncTarget = if (message.senderPublicKey == userHexEncodedPublicKey) group.id else null
|
|
||||||
return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, 0, false, null, quote, null, signalLinkPreviews, null, syncTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pollForNewMessages(): Promise<Unit, Exception> {
|
|
||||||
if (isPollOngoing) { return Promise.of(Unit) }
|
|
||||||
isPollOngoing = true
|
|
||||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
|
||||||
FileServerAPI.configure(userHexEncodedPublicKey, userPrivateKey, apiDB)
|
|
||||||
// Kovenant propagates a context to chained promises, so LokiPublicChatAPI.sharedContext should be used for all of the below
|
|
||||||
val promise = api.getMessages(group.channel, group.server).bind(PublicChatAPI.sharedContext) { messages ->
|
|
||||||
Promise.of(messages)
|
|
||||||
}
|
|
||||||
promise.successBackground { messages ->
|
|
||||||
// Process messages in the background
|
|
||||||
messages.forEach { message ->
|
|
||||||
// If the sender of the current message is not a slave device, set the display name in the database
|
|
||||||
val senderDisplayName = "${message.displayName} (...${message.senderPublicKey.takeLast(8)})"
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.senderPublicKey, senderDisplayName)
|
|
||||||
val senderHexEncodedPublicKey = message.senderPublicKey
|
|
||||||
val serviceDataMessage = getDataMessage(message)
|
|
||||||
val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.serverTimestamp, false)
|
|
||||||
if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
|
|
||||||
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
|
||||||
} else {
|
|
||||||
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
|
||||||
}
|
|
||||||
// Update profile picture if needed
|
|
||||||
val senderAsRecipient = Recipient.from(context, Address.fromSerialized(senderHexEncodedPublicKey), false)
|
|
||||||
if (message.profilePicture != null && message.profilePicture!!.url.isNotEmpty()) {
|
|
||||||
val profileKey = message.profilePicture!!.profileKey
|
|
||||||
val url = message.profilePicture!!.url
|
|
||||||
if (senderAsRecipient.profileKey == null || !MessageDigest.isEqual(senderAsRecipient.profileKey, profileKey)) {
|
|
||||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
|
||||||
database.setProfileKey(senderAsRecipient, profileKey)
|
|
||||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isCaughtUp = true
|
|
||||||
isPollOngoing = false
|
|
||||||
}
|
|
||||||
promise.fail {
|
|
||||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${group.channel} on server: ${group.server}.")
|
|
||||||
isPollOngoing = false
|
|
||||||
}
|
|
||||||
return promise.map { Unit }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForDisplayNames() {
|
|
||||||
if (displayNameUpdatees.isEmpty()) { return }
|
|
||||||
val hexEncodedPublicKeys = displayNameUpdatees
|
|
||||||
displayNameUpdatees = setOf()
|
|
||||||
api.getDisplayNames(hexEncodedPublicKeys, group.server).successBackground { mapping ->
|
|
||||||
for (pair in mapping.entries) {
|
|
||||||
val senderDisplayName = "${pair.value} (...${pair.key.takeLast(8)})"
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, pair.key, senderDisplayName)
|
|
||||||
}
|
|
||||||
}.fail {
|
|
||||||
displayNameUpdatees = displayNameUpdatees.union(hexEncodedPublicKeys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForDeletedMessages() {
|
|
||||||
api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs ->
|
|
||||||
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
|
|
||||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) }
|
|
||||||
val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context)
|
|
||||||
val mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context)
|
|
||||||
deletedMessageIDs.forEach {
|
|
||||||
smsMessageDatabase.deleteMessage(it)
|
|
||||||
mmsMessageDatabase.delete(it)
|
|
||||||
}
|
|
||||||
}.fail {
|
|
||||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${group.channel} on server: ${group.server}.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForModerators() {
|
|
||||||
api.getModerators(group.channel, group.server)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -3,16 +3,15 @@ package org.thoughtcrime.securesms.loki.utilities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||||
import java.lang.Exception
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import java.lang.IllegalStateException
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import kotlin.jvm.Throws
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
|
|
||||||
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
||||||
object OpenGroupUtilities {
|
object OpenGroupUtilities {
|
||||||
@ -22,29 +21,27 @@ object OpenGroupUtilities {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun addGroup(context: Context, url: String, channel: Long): PublicChat {
|
fun addGroup(context: Context, url: String, channel: Long): OpenGroup {
|
||||||
// Check for an existing group.
|
// Check for an existing group.
|
||||||
val groupID = PublicChat.getId(channel, url)
|
val groupID = PublicChat.getId(channel, url)
|
||||||
val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
|
val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
|
||||||
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||||
if (openGroup != null) { return openGroup }
|
if (openGroup != null) { return OpenGroup.from(openGroup) }
|
||||||
|
|
||||||
// Add the new group.
|
// Add the new group.
|
||||||
val application = ApplicationContext.getInstance(context)
|
val application = ApplicationContext.getInstance(context)
|
||||||
val displayName = TextSecurePreferences.getProfileName(context)
|
val displayName = TextSecurePreferences.getProfileName(context)
|
||||||
val lokiPublicChatAPI = application.publicChatAPI
|
|
||||||
?: throw IllegalStateException("LokiPublicChatAPI is not initialized.")
|
|
||||||
|
|
||||||
val group = application.publicChatManager.addChat(url, channel)
|
val group = application.publicChatManager.addChat(url, channel)
|
||||||
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(channel, url)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(channel, url)
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(channel, url)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(channel, url)
|
||||||
lokiPublicChatAPI.getMessages(channel, url)
|
OpenGroupAPI.getMessages(channel, url)
|
||||||
lokiPublicChatAPI.setDisplayName(displayName, url)
|
OpenGroupAPI.setDisplayName(displayName, url)
|
||||||
lokiPublicChatAPI.join(channel, url)
|
OpenGroupAPI.join(channel, url)
|
||||||
val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(context)
|
val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(context)
|
||||||
val profileUrl: String? = TextSecurePreferences.getProfilePictureURL(context)
|
val profileUrl: String? = TextSecurePreferences.getProfilePictureURL(context)
|
||||||
lokiPublicChatAPI.setProfilePicture(url, profileKey, profileUrl)
|
OpenGroupAPI.setProfilePicture(url, profileKey, profileUrl)
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,18 +55,15 @@ object OpenGroupUtilities {
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun updateGroupInfo(context: Context, url: String, channel: Long) {
|
fun updateGroupInfo(context: Context, url: String, channel: Long) {
|
||||||
val publicChatAPI = ApplicationContext.getInstance(context).publicChatAPI
|
|
||||||
?: throw IllegalStateException("Public chat API is not initialized!")
|
|
||||||
|
|
||||||
// Check if open group has a related DB record.
|
// Check if open group has a related DB record.
|
||||||
val groupId = GroupUtil.getEncodedOpenGroupID(PublicChat.getId(channel, url).toByteArray())
|
val groupId = GroupUtil.getEncodedOpenGroupID(PublicChat.getId(channel, url).toByteArray())
|
||||||
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
|
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
|
||||||
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
|
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
|
||||||
}
|
}
|
||||||
|
|
||||||
val info = publicChatAPI.getChannelInfo(channel, url).get()
|
val info = OpenGroupAPI.getChannelInfo(channel, url).get()
|
||||||
|
|
||||||
publicChatAPI.updateProfileIfNeeded(channel, url, groupId, info, false)
|
OpenGroupAPI.updateProfileIfNeeded(channel, url, groupId, info, false)
|
||||||
|
|
||||||
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@ import android.view.ViewGroup
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI
|
|
||||||
import org.session.libsignal.service.loki.utilities.mentions.Mention
|
import org.session.libsignal.service.loki.utilities.mentions.Mention
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
var mentionCandidate = Mention("", "")
|
var mentionCandidate = Mention("", "")
|
||||||
@ -38,7 +38,7 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr:
|
|||||||
profilePictureView.glide = glide!!
|
profilePictureView.glide = glide!!
|
||||||
profilePictureView.update()
|
profilePictureView.update()
|
||||||
if (publicChatServer != null && publicChatChannel != null) {
|
if (publicChatServer != null && publicChatChannel != null) {
|
||||||
val isUserModerator = PublicChatAPI.isUserModerator(mentionCandidate.publicKey, publicChatChannel!!, publicChatServer!!)
|
val isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, publicChatChannel!!, publicChatServer!!)
|
||||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||||
} else {
|
} else {
|
||||||
moderatorIconImageView.visibility = View.GONE
|
moderatorIconImageView.visibility = View.GONE
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.session.libsession.messaging.opengroups
|
package org.session.libsession.messaging.opengroups
|
||||||
|
|
||||||
|
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
|
||||||
data class OpenGroup(
|
data class OpenGroup(
|
||||||
@ -13,6 +14,9 @@ data class OpenGroup(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@JvmStatic fun from(publicChat: PublicChat): OpenGroup =
|
||||||
|
OpenGroup(publicChat.channel, publicChat.server, publicChat.displayName, publicChat.isDeletable)
|
||||||
|
|
||||||
@JvmStatic fun getId(channel: Long, server: String): String {
|
@JvmStatic fun getId(channel: Long, server: String): String {
|
||||||
return "$server.$channel"
|
return "$server.$channel"
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,13 @@ import nl.komponents.kovenant.deferred
|
|||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import nl.komponents.kovenant.then
|
import nl.komponents.kovenant.then
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
|
||||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||||
|
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.*
|
|
||||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities
|
import org.session.libsignal.service.loki.utilities.DownloadUtilities
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||||
|
import org.session.libsignal.utilities.*
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -156,6 +154,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
||||||
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
@ -188,6 +187,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
||||||
val deferred = deferred<OpenGroupMessage, Exception>()
|
val deferred = deferred<OpenGroupMessage, Exception>()
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
@ -252,6 +252,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json ->
|
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json ->
|
||||||
try {
|
try {
|
||||||
@ -270,6 +271,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
|
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
|
||||||
return retryIfNeeded(maxRetryCount) {
|
return retryIfNeeded(maxRetryCount) {
|
||||||
val parameters = mapOf( "include_annotations" to 1 )
|
val parameters = mapOf( "include_annotations" to 1 )
|
||||||
@ -294,6 +296,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
storage.setUserCount(channel, server, info.memberCount)
|
storage.setUserCount(channel, server, info.memberCount)
|
||||||
@ -307,6 +310,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
||||||
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
||||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
||||||
@ -323,6 +327,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
||||||
return retryIfNeeded(maxRetryCount) {
|
return retryIfNeeded(maxRetryCount) {
|
||||||
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
||||||
@ -331,6 +336,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
||||||
return retryIfNeeded(maxRetryCount) {
|
return retryIfNeeded(maxRetryCount) {
|
||||||
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
||||||
@ -348,6 +354,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
||||||
return getUserProfiles(publicKeys, server, false).map(sharedContext) { json ->
|
return getUserProfiles(publicKeys, server, false).map(sharedContext) { json ->
|
||||||
val mapping = mutableMapOf<String, String>()
|
val mapping = mutableMapOf<String, String>()
|
||||||
@ -362,12 +369,14 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
||||||
Log.d("Loki", "Updating display name on server: $server.")
|
Log.d("Loki", "Updating display name on server: $server.")
|
||||||
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
||||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
||||||
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,6 @@ object MessageReceiver {
|
|||||||
}
|
}
|
||||||
// Don't process the envelope any further if the message has been handled already
|
// Don't process the envelope any further if the message has been handled already
|
||||||
if (storage.isMessageDuplicated(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage
|
if (storage.isMessageDuplicated(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage
|
||||||
storage.addReceivedMessageTimestamp(envelope.timestamp)
|
|
||||||
// Don't process the envelope any further if the sender is blocked
|
// Don't process the envelope any further if the sender is blocked
|
||||||
if (isBlock(sender!!)) throw Error.SenderBlocked
|
if (isBlock(sender!!)) throw Error.SenderBlocked
|
||||||
// Parse the proto
|
// Parse the proto
|
||||||
|
@ -25,6 +25,11 @@ public class Debouncer {
|
|||||||
this.threshold = threshold;
|
this.threshold = threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Debouncer(Handler handler, long threshold) {
|
||||||
|
this.handler = handler;
|
||||||
|
this.threshold = threshold;
|
||||||
|
}
|
||||||
|
|
||||||
public void publish(Runnable runnable) {
|
public void publish(Runnable runnable) {
|
||||||
handler.removeCallbacksAndMessages(null);
|
handler.removeCallbacksAndMessages(null);
|
||||||
handler.postDelayed(runnable, threshold);
|
handler.postDelayed(runnable, threshold);
|
||||||
|
@ -1,386 +0,0 @@
|
|||||||
package org.session.libsignal.service.loki.api.opengroups
|
|
||||||
|
|
||||||
import nl.komponents.kovenant.Kovenant
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.deferred
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import nl.komponents.kovenant.then
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
|
||||||
import org.session.libsignal.service.loki.api.LokiDotNetAPI
|
|
||||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
|
||||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
|
||||||
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol
|
|
||||||
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol
|
|
||||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities
|
|
||||||
import org.session.libsignal.utilities.*
|
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class PublicChatAPI(userPublicKey: String, private val userPrivateKey: ByteArray, private val apiDatabase: LokiAPIDatabaseProtocol,
|
|
||||||
private val userDatabase: LokiUserDatabaseProtocol, private val openGroupDatabase: LokiOpenGroupDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, apiDatabase) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val moderators: HashMap<String, HashMap<Long, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
|
||||||
val sharedContext = Kovenant.createContext()
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
private val fallbackBatchCount = 64
|
|
||||||
private val maxRetryCount = 8
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Convenience
|
|
||||||
private val channelInfoType = "net.patter-app.settings"
|
|
||||||
private val attachmentType = "net.app.core.oembed"
|
|
||||||
@JvmStatic
|
|
||||||
val publicChatMessageType = "network.loki.messenger.publicChat"
|
|
||||||
@JvmStatic
|
|
||||||
val profilePictureType = "network.loki.messenger.avatar"
|
|
||||||
|
|
||||||
fun getDefaultChats(): List<PublicChat> {
|
|
||||||
return listOf() // Don't auto-join any open groups right now
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
|
|
||||||
if (moderators[server] != null && moderators[server]!![channel] != null) {
|
|
||||||
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Public API
|
|
||||||
fun getMessages(channel: Long, server: String): Promise<List<PublicChatMessage>, Exception> {
|
|
||||||
Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.")
|
|
||||||
val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
|
|
||||||
val lastMessageServerID = apiDatabase.getLastMessageServerID(channel, server)
|
|
||||||
if (lastMessageServerID != null) {
|
|
||||||
parameters["since_id"] = lastMessageServerID
|
|
||||||
} else {
|
|
||||||
parameters["count"] = fallbackBatchCount
|
|
||||||
parameters["include_deleted"] = 0
|
|
||||||
}
|
|
||||||
return execute(HTTPVerb.GET, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val data = json["data"] as List<Map<*, *>>
|
|
||||||
val messages = data.mapNotNull { message ->
|
|
||||||
try {
|
|
||||||
val isDeleted = message["is_deleted"] as? Boolean ?: false
|
|
||||||
if (isDeleted) { return@mapNotNull null }
|
|
||||||
// Ignore messages without annotations
|
|
||||||
if (message["annotations"] == null) { return@mapNotNull null }
|
|
||||||
val annotation = (message["annotations"] as List<Map<*, *>>).find {
|
|
||||||
((it["type"] as? String ?: "") == publicChatMessageType) && it["value"] != null
|
|
||||||
} ?: return@mapNotNull null
|
|
||||||
val value = annotation["value"] as Map<*, *>
|
|
||||||
val serverID = message["id"] as? Long ?: (message["id"] as? Int)?.toLong() ?: (message["id"] as String).toLong()
|
|
||||||
val user = message["user"] as Map<*, *>
|
|
||||||
val publicKey = user["username"] as String
|
|
||||||
val displayName = user["name"] as? String ?: "Anonymous"
|
|
||||||
var profilePicture: PublicChatMessage.ProfilePicture? = null
|
|
||||||
if (user["annotations"] != null) {
|
|
||||||
val profilePictureAnnotation = (user["annotations"] as List<Map< *, *>>).find {
|
|
||||||
((it["type"] as? String ?: "") == profilePictureType) && it["value"] != null
|
|
||||||
}
|
|
||||||
val profilePictureAnnotationValue = profilePictureAnnotation?.get("value") as? Map<*, *>
|
|
||||||
if (profilePictureAnnotationValue != null && profilePictureAnnotationValue["profileKey"] != null && profilePictureAnnotationValue["url"] != null) {
|
|
||||||
try {
|
|
||||||
val profileKey = Base64.decode(profilePictureAnnotationValue["profileKey"] as String)
|
|
||||||
val url = profilePictureAnnotationValue["url"] as String
|
|
||||||
profilePicture = PublicChatMessage.ProfilePicture(profileKey, url)
|
|
||||||
} catch (e: Exception) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Suppress("NAME_SHADOWING") val body = message["text"] as String
|
|
||||||
val timestamp = value["timestamp"] as? Long ?: (value["timestamp"] as? Int)?.toLong() ?: (value["timestamp"] as String).toLong()
|
|
||||||
var quote: PublicChatMessage.Quote? = null
|
|
||||||
if (value["quote"] != null) {
|
|
||||||
val replyTo = message["reply_to"] as? Long ?: (message["reply_to"] as? Int)?.toLong() ?: (message["reply_to"] as String).toLong()
|
|
||||||
val quoteAnnotation = value["quote"] as? Map<*, *>
|
|
||||||
val quoteTimestamp = quoteAnnotation?.get("id") as? Long ?: (quoteAnnotation?.get("id") as? Int)?.toLong() ?: (quoteAnnotation?.get("id") as? String)?.toLong() ?: 0L
|
|
||||||
val author = quoteAnnotation?.get("author") as? String
|
|
||||||
val text = quoteAnnotation?.get("text") as? String
|
|
||||||
quote = if (quoteTimestamp > 0L && author != null && text != null) PublicChatMessage.Quote(quoteTimestamp, author, text, replyTo) else null
|
|
||||||
}
|
|
||||||
val attachmentsAsJSON = (message["annotations"] as List<Map<*, *>>).filter {
|
|
||||||
((it["type"] as? String ?: "") == attachmentType) && it["value"] != null
|
|
||||||
}
|
|
||||||
val attachments = attachmentsAsJSON.mapNotNull { it["value"] as? Map<*, *> }.mapNotNull { attachmentAsJSON ->
|
|
||||||
try {
|
|
||||||
val kindAsString = attachmentAsJSON["lokiType"] as String
|
|
||||||
val kind = PublicChatMessage.Attachment.Kind.values().first { it.rawValue == kindAsString }
|
|
||||||
val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong()
|
|
||||||
val contentType = attachmentAsJSON["contentType"] as String
|
|
||||||
val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt()
|
|
||||||
val fileName = attachmentAsJSON["fileName"] as String
|
|
||||||
val flags = 0
|
|
||||||
val url = attachmentAsJSON["url"] as String
|
|
||||||
val caption = attachmentAsJSON["caption"] as? String
|
|
||||||
val linkPreviewURL = attachmentAsJSON["linkPreviewUrl"] as? String
|
|
||||||
val linkPreviewTitle = attachmentAsJSON["linkPreviewTitle"] as? String
|
|
||||||
if (kind == PublicChatMessage.Attachment.Kind.LinkPreview && (linkPreviewURL == null || linkPreviewTitle == null)) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
PublicChatMessage.Attachment(kind, server, id, contentType, size, fileName, flags, 0, 0, caption, url, linkPreviewURL, linkPreviewTitle)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki","Couldn't parse attachment due to error: $e.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Set the last message server ID here to avoid the situation where a message doesn't have a valid signature and this function is called over and over
|
|
||||||
@Suppress("NAME_SHADOWING") val lastMessageServerID = apiDatabase.getLastMessageServerID(channel, server)
|
|
||||||
if (serverID > lastMessageServerID ?: 0) { apiDatabase.setLastMessageServerID(channel, server, serverID) }
|
|
||||||
val hexEncodedSignature = value["sig"] as String
|
|
||||||
val signatureVersion = value["sigver"] as? Long ?: (value["sigver"] as? Int)?.toLong() ?: (value["sigver"] as String).toLong()
|
|
||||||
val signature = PublicChatMessage.Signature(Hex.fromStringCondensed(hexEncodedSignature), signatureVersion)
|
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
|
||||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
|
||||||
val dateAsString = message["created_at"] as String
|
|
||||||
val serverTimestamp = format.parse(dateAsString).time
|
|
||||||
// Verify the message
|
|
||||||
val groupMessage = PublicChatMessage(serverID, publicKey, displayName, body, timestamp, publicChatMessageType, quote, attachments, profilePicture, signature, serverTimestamp)
|
|
||||||
if (groupMessage.hasValidSignature()) groupMessage else null
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server from: ${JsonUtil.toJson(message)}. Exception: ${exception.message}")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
}.sortedBy { it.serverTimestamp }
|
|
||||||
messages
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse messages for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
|
||||||
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
|
||||||
val parameters = mutableMapOf<String, Any>()
|
|
||||||
val lastDeletionServerID = apiDatabase.getLastDeletionServerID(channel, server)
|
|
||||||
if (lastDeletionServerID != null) {
|
|
||||||
parameters["since_id"] = lastDeletionServerID
|
|
||||||
} else {
|
|
||||||
parameters["count"] = fallbackBatchCount
|
|
||||||
}
|
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/deletes", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val deletedMessageServerIDs = (json["data"] as List<Map<*, *>>).mapNotNull { deletion ->
|
|
||||||
try {
|
|
||||||
val serverID = deletion["id"] as? Long ?: (deletion["id"] as? Int)?.toLong() ?: (deletion["id"] as String).toLong()
|
|
||||||
val messageServerID = deletion["message_id"] as? Long ?: (deletion["message_id"] as? Int)?.toLong() ?: (deletion["message_id"] as String).toLong()
|
|
||||||
@Suppress("NAME_SHADOWING") val lastDeletionServerID = apiDatabase.getLastDeletionServerID(channel, server)
|
|
||||||
if (serverID > (lastDeletionServerID ?: 0)) { apiDatabase.setLastDeletionServerID(channel, server, serverID) }
|
|
||||||
messageServerID
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse deleted message for open group with ID: $channel on server: $server. Exception: ${exception.message}")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deletedMessageServerIDs
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse deleted messages for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessage(message: PublicChatMessage, channel: Long, server: String): Promise<PublicChatMessage, Exception> {
|
|
||||||
val deferred = deferred<PublicChatMessage, Exception>()
|
|
||||||
ThreadUtils.queue {
|
|
||||||
val signedMessage = message.sign(userPrivateKey)
|
|
||||||
if (signedMessage == null) {
|
|
||||||
deferred.reject(SnodeAPI.Error.MessageSigningFailed)
|
|
||||||
} else {
|
|
||||||
retryIfNeeded(maxRetryCount) {
|
|
||||||
Log.d("Loki", "Sending message to open group with ID: $channel on server: $server.")
|
|
||||||
val parameters = signedMessage.toJSON()
|
|
||||||
execute(HTTPVerb.POST, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val data = json["data"] as Map<*, *>
|
|
||||||
val serverID = (data["id"] as? Long) ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as String).toLong()
|
|
||||||
val displayName = userDatabase.getDisplayName(userPublicKey) ?: "Anonymous"
|
|
||||||
val text = data["text"] as String
|
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
|
||||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
|
||||||
val dateAsString = data["created_at"] as String
|
|
||||||
val timestamp = format.parse(dateAsString).time
|
|
||||||
@Suppress("NAME_SHADOWING") val message = PublicChatMessage(serverID, userPublicKey, displayName, text, timestamp, publicChatMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
|
|
||||||
message
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.success {
|
|
||||||
deferred.resolve(it)
|
|
||||||
}.fail {
|
|
||||||
deferred.reject(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deferred.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteMessage(messageServerID: Long, channel: Long, server: String, isSentByUser: Boolean): Promise<Long, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
val isModerationRequest = !isSentByUser
|
|
||||||
Log.d("Loki", "Deleting message with ID: $messageServerID from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
|
||||||
val endpoint = if (isSentByUser) "channels/$channel/messages/$messageServerID" else "loki/v1/moderation/message/$messageServerID"
|
|
||||||
execute(HTTPVerb.DELETE, server, endpoint, isJSONRequired = false).then {
|
|
||||||
Log.d("Loki", "Deleted message with ID: $messageServerID from open group with ID: $channel on server: $server.")
|
|
||||||
messageServerID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
val isModerationRequest = !isSentByUser
|
|
||||||
val parameters = mapOf( "ids" to messageServerIDs.joinToString(",") )
|
|
||||||
Log.d("Loki", "Deleting messages with IDs: ${messageServerIDs.joinToString()} from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
|
||||||
val endpoint = if (isSentByUser) "loki/v1/messages" else "loki/v1/moderation/messages"
|
|
||||||
execute(HTTPVerb.DELETE, server, endpoint, parameters = parameters, isJSONRequired = false).then { json ->
|
|
||||||
Log.d("Loki", "Deleted messages with IDs: $messageServerIDs from open group with ID: $channel on server: $server.")
|
|
||||||
messageServerIDs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
@Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List<String>
|
|
||||||
val moderatorsAsSet = moderators.orEmpty().toSet()
|
|
||||||
if (Companion.moderators[server] != null) {
|
|
||||||
Companion.moderators[server]!![channel] = moderatorsAsSet
|
|
||||||
} else {
|
|
||||||
Companion.moderators[server] = hashMapOf( channel to moderatorsAsSet )
|
|
||||||
}
|
|
||||||
moderatorsAsSet
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse moderators for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChannelInfo(channel: Long, server: String): Promise<PublicChatInfo, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
val parameters = mapOf( "include_annotations" to 1 )
|
|
||||||
execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val data = json["data"] as Map<*, *>
|
|
||||||
val annotations = data["annotations"] as List<Map<*, *>>
|
|
||||||
val annotation = annotations.find { (it["type"] as? String ?: "") == channelInfoType } ?: throw SnodeAPI.Error.ParsingFailed
|
|
||||||
val info = annotation["value"] as Map<*, *>
|
|
||||||
val displayName = info["name"] as String
|
|
||||||
val countInfo = data["counts"] as Map<*, *>
|
|
||||||
val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt()
|
|
||||||
val profilePictureURL = info["avatar"] as String
|
|
||||||
val publicChatInfo = PublicChatInfo(displayName, profilePictureURL, memberCount)
|
|
||||||
apiDatabase.setUserCount(channel, server, memberCount)
|
|
||||||
publicChatInfo
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: PublicChatInfo, isForcedUpdate: Boolean) {
|
|
||||||
apiDatabase.setUserCount(channel, server, info.memberCount)
|
|
||||||
openGroupDatabase.updateTitle(groupID, info.displayName)
|
|
||||||
// Download and update profile picture if needed
|
|
||||||
val oldProfilePictureURL = apiDatabase.getOpenGroupProfilePictureURL(channel, server)
|
|
||||||
if (isForcedUpdate || oldProfilePictureURL != info.profilePictureURL) {
|
|
||||||
val profilePictureAsByteArray = downloadOpenGroupProfilePicture(server, info.profilePictureURL) ?: return
|
|
||||||
openGroupDatabase.updateProfilePicture(groupID, profilePictureAsByteArray)
|
|
||||||
apiDatabase.setOpenGroupProfilePictureURL(channel, server, info.profilePictureURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
|
||||||
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
|
||||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
try {
|
|
||||||
DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null)
|
|
||||||
Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"")
|
|
||||||
return outputStream.toByteArray()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Failed to download open group profile picture from \"$url\" due to error: $e.")
|
|
||||||
return null
|
|
||||||
} finally {
|
|
||||||
outputStream.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
|
||||||
Log.d("Loki", "Joined channel with ID: $channel on server: $server.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
|
||||||
Log.d("Loki", "Left channel with ID: $channel on server: $server.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {
|
|
||||||
Log.d("Loki", "Banned user with ID: $publicKey from $server")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
|
||||||
return getUserProfiles(publicKeys, server, false).map(sharedContext) { json ->
|
|
||||||
val mapping = mutableMapOf<String, String>()
|
|
||||||
for (user in json) {
|
|
||||||
if (user["username"] != null) {
|
|
||||||
val publicKey = user["username"] as String
|
|
||||||
val displayName = user["name"] as? String ?: "Anonymous"
|
|
||||||
mapping[publicKey] = displayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapping
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
|
||||||
Log.d("Loki", "Updating display name on server: $server.")
|
|
||||||
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
|
||||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
|
||||||
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setProfilePicture(server: String, profileKey: String, url: String?): Promise<Unit, Exception> {
|
|
||||||
Log.d("Loki", "Updating profile picture on server: $server.")
|
|
||||||
val value = when (url) {
|
|
||||||
null -> null
|
|
||||||
else -> mapOf( "profileKey" to profileKey, "url" to url )
|
|
||||||
}
|
|
||||||
// TODO: This may actually completely replace the annotations, have to double check it
|
|
||||||
return setSelfAnnotation(server, profilePictureType, value).map { Unit }.fail {
|
|
||||||
Log.d("Loki", "Failed to update profile picture due to error: $it.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user