moxxy/lib/service/events.dart

1451 lines
40 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/service/avatars.dart';
import 'package:moxxyv2/service/blocking.dart';
import 'package:moxxyv2/service/connectivity.dart';
import 'package:moxxyv2/service/contacts.dart';
import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/helpers.dart';
import 'package:moxxyv2/service/httpfiletransfer/helpers.dart';
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
import 'package:moxxyv2/service/httpfiletransfer/jobs.dart';
import 'package:moxxyv2/service/httpfiletransfer/location.dart';
import 'package:moxxyv2/service/language.dart';
import 'package:moxxyv2/service/message.dart';
import 'package:moxxyv2/service/notifications.dart';
import 'package:moxxyv2/service/omemo/omemo.dart';
import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/service/reactions.dart';
import 'package:moxxyv2/service/roster.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/service/stickers.dart';
import 'package:moxxyv2/service/storage.dart';
import 'package:moxxyv2/service/xmpp.dart';
import 'package:moxxyv2/service/xmpp_state.dart';
import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/debug.dart' as debug;
import 'package:moxxyv2/shared/error_types.dart';
import 'package:moxxyv2/shared/eventhandler.dart';
import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/shared/models/file_metadata.dart';
import 'package:moxxyv2/shared/models/preferences.dart';
import 'package:moxxyv2/shared/models/reaction_group.dart';
import 'package:moxxyv2/shared/models/sticker.dart' as sticker;
import 'package:moxxyv2/shared/models/sticker_pack.dart' as sticker_pack;
import 'package:moxxyv2/shared/models/xmpp_state.dart';
import 'package:moxxyv2/shared/synchronized_queue.dart';
//import 'package:permission_handler/permission_handler.dart';
void setupBackgroundEventHandler() {
final handler = EventHandler()
..addMatchers([
EventTypeMatcher<LoginCommand>(performLogin),
EventTypeMatcher<PerformPreStartCommand>(performPreStart),
EventTypeMatcher<AddConversationCommand>(performAddConversation),
EventTypeMatcher<AddContactCommand>(performAddContact),
EventTypeMatcher<RemoveContactCommand>(performRemoveContact),
EventTypeMatcher<SetOpenConversationCommand>(performSetOpenConversation),
EventTypeMatcher<SendMessageCommand>(performSendMessage),
EventTypeMatcher<BlockJidCommand>(performBlockJid),
EventTypeMatcher<UnblockJidCommand>(performUnblockJid),
EventTypeMatcher<UnblockAllCommand>(performUnblockAll),
EventTypeMatcher<SetCSIStateCommand>(performSetCSIState),
EventTypeMatcher<SetPreferencesCommand>(performSetPreferences),
EventTypeMatcher<RequestDownloadCommand>(performRequestDownload),
EventTypeMatcher<SetAvatarCommand>(performSetAvatar),
EventTypeMatcher<SetShareOnlineStatusCommand>(
performSetShareOnlineStatus,
),
EventTypeMatcher<CloseConversationCommand>(performCloseConversation),
EventTypeMatcher<SendChatStateCommand>(performSendChatState),
EventTypeMatcher<GetFeaturesCommand>(performGetFeatures),
EventTypeMatcher<SignOutCommand>(performSignOut),
EventTypeMatcher<SendFilesCommand>(performSendFiles),
EventTypeMatcher<SetConversationMuteStatusCommand>(performSetMuteState),
EventTypeMatcher<GetConversationOmemoFingerprintsCommand>(
performGetOmemoFingerprints,
),
EventTypeMatcher<SetOmemoDeviceEnabledCommand>(performEnableOmemoKey),
EventTypeMatcher<RecreateSessionsCommand>(performRecreateSessions),
EventTypeMatcher<SetOmemoEnabledCommand>(performSetOmemoEnabled),
EventTypeMatcher<GetOwnOmemoFingerprintsCommand>(
performGetOwnOmemoFingerprints,
),
EventTypeMatcher<RemoveOwnDeviceCommand>(performRemoveOwnDevice),
EventTypeMatcher<RegenerateOwnDeviceCommand>(performRegenerateOwnDevice),
EventTypeMatcher<RetractMessageCommentCommand>(performMessageRetraction),
EventTypeMatcher<MarkConversationAsReadCommand>(
performMarkConversationAsRead,
),
EventTypeMatcher<MarkMessageAsReadCommand>(performMarkMessageAsRead),
EventTypeMatcher<AddReactionToMessageCommand>(performAddMessageReaction),
EventTypeMatcher<RemoveReactionFromMessageCommand>(
performRemoveMessageReaction,
),
EventTypeMatcher<MarkOmemoDeviceAsVerifiedCommand>(
performMarkDeviceVerified,
),
EventTypeMatcher<ImportStickerPackCommand>(performImportStickerPack),
EventTypeMatcher<SendStickerCommand>(performSendSticker),
EventTypeMatcher<RemoveStickerPackCommand>(performRemoveStickerPack),
EventTypeMatcher<FetchStickerPackCommand>(performFetchStickerPack),
EventTypeMatcher<InstallStickerPackCommand>(performStickerPackInstall),
EventTypeMatcher<GetBlocklistCommand>(performGetBlocklist),
EventTypeMatcher<GetPagedMessagesCommand>(performGetPagedMessages),
EventTypeMatcher<GetPagedSharedMediaCommand>(performGetPagedSharedMedia),
EventTypeMatcher<GetReactionsForMessageCommand>(performGetReactions),
EventTypeMatcher<RequestAvatarForJidCommand>(performRequestAvatarForJid),
EventTypeMatcher<GetStorageUsageCommand>(performGetStorageUsage),
EventTypeMatcher<DeleteOldMediaFilesCommand>(performOldMediaFileDeletion),
EventTypeMatcher<GetPagedStickerPackCommand>(performGetPagedStickerPacks),
EventTypeMatcher<GetStickerPackByIdCommand>(performGetStickerPackById),
EventTypeMatcher<DebugCommand>(performDebugCommand),
]);
GetIt.I.registerSingleton<EventHandler>(handler);
GetIt.I.registerSingleton<SynchronizedQueue<Map<String, dynamic>?>>(
SynchronizedQueue<Map<String, dynamic>?>(handleUIEvent),
);
}
Future<void> performLogin(LoginCommand command, {dynamic extra}) async {
final id = extra as String;
GetIt.I.get<Logger>().fine('Performing login...');
final result = await GetIt.I.get<XmppService>().connectAwaitable(
ConnectionSettings(
jid: JID.fromString(command.jid),
password: command.password,
),
true,
);
GetIt.I.get<Logger>().fine('Login done');
// ignore: avoid_dynamic_calls
final xc = GetIt.I.get<XmppConnection>();
if (result.isType<bool>() && result.get<bool>()) {
final preferences =
await GetIt.I.get<PreferencesService>().getPreferences();
final settings = xc.connectionSettings;
sendEvent(
LoginSuccessfulEvent(
jid: settings.jid.toString(),
preStart: await _buildPreStartDoneEvent(preferences),
),
id: id,
);
} else {
await xc.reconnectionPolicy.setShouldReconnect(false);
sendEvent(
LoginFailureEvent(
reason: xmppErrorToTranslatableString(result.get<XmppError>()),
),
id: id,
);
}
}
Future<PreStartDoneEvent> _buildPreStartDoneEvent(
PreferencesState preferences,
) async {
final xss = GetIt.I.get<XmppStateService>();
final state = await xss.getXmppState();
await GetIt.I.get<RosterService>().loadRosterFromDatabase();
// Check some permissions
// TODO(Unknown): Do we still need this permission?
// final storagePerm = await Permission.storage.status;
// final permissions = List<int>.empty(growable: true);
// if (storagePerm.isDenied /*&& !state.askedStoragePermission*/) {
// permissions.add(Permission.storage.value);
// await xss.modifyXmppState(
// (state) => state.copyWith(
// askedStoragePermission: true,
// ),
// );
// }
return PreStartDoneEvent(
state: 'logged_in',
jid: state.jid,
displayName: state.displayName ?? state.jid!.split('@').first,
avatarUrl: state.avatarUrl,
avatarHash: state.avatarHash,
permissionsToRequest: [],
preferences: preferences,
conversations:
(await GetIt.I.get<ConversationService>().loadConversations())
.where((c) => c.open)
.toList(),
roster: await GetIt.I.get<RosterService>().loadRosterFromDatabase(),
);
}
Future<void> performPreStart(
PerformPreStartCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
// Set the locale very early
GetIt.I.get<LanguageService>().defaultLocale = command.systemLocaleCode;
if (preferences.languageLocaleCode == 'default') {
LocaleSettings.setLocaleRaw(command.systemLocaleCode);
} else {
LocaleSettings.setLocaleRaw(preferences.languageLocaleCode);
}
GetIt.I.get<XmppService>().setNotificationText(
await GetIt.I.get<XmppConnection>().getConnectionState(),
);
final settings = await GetIt.I.get<XmppService>().getConnectionSettings();
if (settings != null) {
sendEvent(
await _buildPreStartDoneEvent(preferences),
id: id,
);
} else {
sendEvent(
PreStartDoneEvent(
state: 'not_logged_in',
permissionsToRequest: List<int>.empty(),
preferences: preferences,
),
id: id,
);
}
}
Future<void> performAddConversation(
AddConversationCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final cs = GetIt.I.get<ConversationService>();
final css = GetIt.I.get<ContactsService>();
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
await cs.createOrUpdateConversation(
command.jid,
create: () async {
// Create
final contactId = await css.getContactIdForJid(command.jid);
final newConversation = await cs.addConversationFromData(
command.title,
null,
stringToConversationType(command.conversationType),
command.avatarUrl,
command.jid,
0,
DateTime.now().millisecondsSinceEpoch,
true,
preferences.defaultMuteState,
preferences.enableOmemoByDefault,
contactId,
await css.getProfilePicturePathForJid(command.jid),
await css.getContactDisplayName(contactId),
);
sendEvent(
ConversationAddedEvent(
conversation: newConversation,
),
id: id,
);
return newConversation;
},
update: (c) async {
// Update
if (!c.open) {
// Re-open the conversation
final newConversation = await cs.updateConversation(
c.jid,
open: true,
lastChangeTimestamp: DateTime.now().millisecondsSinceEpoch,
);
sendEvent(
ConversationAddedEvent(
conversation: newConversation,
),
id: id,
);
return newConversation;
}
sendEvent(
NoConversationModifiedEvent(),
id: id,
);
return c;
},
);
}
Future<void> performSetOpenConversation(
SetOpenConversationCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<XmppService>().setCurrentlyOpenedChatJid(command.jid ?? '');
// Null just means that the chat has been closed
// Empty string JID for notes to self
if (command.jid != null && command.jid != '') {
await GetIt.I
.get<NotificationsService>()
.dismissNotificationsByJid(command.jid!);
}
}
Future<void> performSendMessage(
SendMessageCommand command, {
dynamic extra,
}) async {
final xs = GetIt.I.get<XmppService>();
if (command.editSid != null && command.editId != null) {
assert(
command.recipients.length == 1,
'Edits must not be sent to multiple recipients',
);
await xs.sendMessageCorrection(
command.editId!,
command.body,
command.editSid!,
command.recipients.first,
command.chatState.isNotEmpty
? ChatState.fromName(command.chatState)
: null,
);
return;
}
await xs.sendMessage(
body: command.body,
recipients: command.recipients,
chatState: command.chatState.isNotEmpty
? ChatState.fromName(command.chatState)
: null,
quotedMessage: command.quotedMessage,
currentConversationJid: command.currentConversationJid,
commandId: extra as String,
);
}
Future<void> performBlockJid(BlockJidCommand command, {dynamic extra}) async {
await GetIt.I.get<BlocklistService>().blockJid(command.jid);
}
Future<void> performUnblockJid(
UnblockJidCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<BlocklistService>().unblockJid(command.jid);
}
Future<void> performUnblockAll(
UnblockAllCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<BlocklistService>().unblockAll();
}
Future<void> performSetCSIState(
SetCSIStateCommand command, {
dynamic extra,
}) async {
// Tell the [XmppService] about the app state
GetIt.I.get<XmppService>().setAppState(command.active);
final conn = GetIt.I.get<XmppConnection>();
// Only send the CSI nonza when we're connected
if (await conn.getConnectionState() != XmppConnectionState.connected) return;
final csi = conn.getManagerById<CSIManager>(csiManager)!;
if (command.active) {
await csi.setActive();
} else {
await csi.setInactive();
}
}
Future<void> performSetPreferences(
SetPreferencesCommand command, {
dynamic extra,
}) async {
final ps = GetIt.I.get<PreferencesService>();
final oldPrefs = await ps.getPreferences();
await ps.modifyPreferences((_) => command.preferences);
// Set the logging mode
if (!kDebugMode) {
final enableDebug = command.preferences.debugEnabled;
Logger.root.level = enableDebug ? Level.ALL : Level.INFO;
}
// Scan all contacts if the setting is enabled or disable the database callback
// if it is disabled.
final css = GetIt.I.get<ContactsService>();
if (command.preferences.enableContactIntegration) {
if (!oldPrefs.enableContactIntegration) {
await css.enable();
}
unawaited(css.scanContacts());
} else {
if (oldPrefs.enableContactIntegration) {
await css.disable();
}
}
// TODO(Unknown): Maybe handle this in StickersService
// If sticker visibility was changed, apply the settings to the PubSub node
final pm = GetIt.I
.get<XmppConnection>()
.getManagerById<PubSubManager>(pubsubManager)!;
final ownJid = JID.fromString(
(await GetIt.I.get<XmppStateService>().getXmppState()).jid!,
);
if (command.preferences.isStickersNodePublic &&
!oldPrefs.isStickersNodePublic) {
// Set to open
unawaited(
pm.configure(
ownJid,
stickersXmlns,
const PubSubPublishOptions(
accessModel: 'open',
maxItems: 'max',
),
),
);
} else if (!command.preferences.isStickersNodePublic &&
oldPrefs.isStickersNodePublic) {
// Set to presence
unawaited(
pm.configure(
ownJid,
stickersXmlns,
const PubSubPublishOptions(
accessModel: 'presence',
maxItems: 'max',
),
),
);
}
// Set the locale
final locale = command.preferences.languageLocaleCode == 'default'
? GetIt.I.get<LanguageService>().defaultLocale
: command.preferences.languageLocaleCode;
LocaleSettings.setLocaleRaw(locale);
GetIt.I.get<XmppService>().setNotificationText(
await GetIt.I.get<XmppConnection>().getConnectionState(),
);
}
/// Attempts to achieve a "both" subscription with [jid].
Future<void> _maybeAchieveBothSubscription(String jid) async {
final roster = GetIt.I.get<RosterService>();
final item = await roster.getRosterItemByJid(jid);
if (item != null) {
GetIt.I.get<Logger>().finest(
'Roster item for $jid has subscription "${item.subscription}" with ask "${item.ask}"',
);
// Nothing more to do
if (item.subscription == 'both') {
return;
}
final pm = GetIt.I
.get<XmppConnection>()
.getManagerById<PresenceManager>(presenceManager)!;
switch (item.subscription) {
case 'both':
return;
case 'none':
case 'from':
if (item.ask != 'subscribe') {
// Try to move from "from"/"none" to "both", by going over "From + Pending Out"
await pm.requestSubscription(JID.fromString(item.jid));
}
break;
case 'to':
// Move from "to" to "both"
await pm.acceptSubscriptionRequest(JID.fromString(item.jid));
break;
}
} else {
await roster.addToRosterWrapper('', '', jid, jid.split('@')[0]);
}
}
Future<void> performAddContact(
AddContactCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final jid = command.jid;
final roster = GetIt.I.get<RosterService>();
final inRoster = await roster.isInRoster(jid);
final cs = GetIt.I.get<ConversationService>();
final conversation = await cs.getConversationByJid(jid);
if (conversation != null) {
await cs.createOrUpdateConversation(
jid,
update: (c) async {
final newConversation = await cs.updateConversation(
jid,
open: true,
lastChangeTimestamp: DateTime.now().millisecondsSinceEpoch,
);
sendEvent(
AddContactResultEvent(
conversation: newConversation,
added: !inRoster,
),
id: id,
);
return newConversation;
},
);
// Add to roster, if needed
await _maybeAchieveBothSubscription(jid);
} else {
// We did not have a conversation with that JID.
final info = await GetIt.I
.get<XmppConnection>()
.getDiscoManager()!
.discoInfoQuery(JID.fromString(jid));
var isGroupchat = false;
if (info.isType<DiscoInfo>()) {
isGroupchat = info.get<DiscoInfo>().identities.firstWhereOrNull(
(identity) => identity.category == 'conference',
) !=
null;
} else if (info.isType<RemoteServerNotFoundError>()) {
sendEvent(
ErrorEvent(
errorId: ErrorType.remoteServerNotFound.value,
),
id: id,
);
return;
} else if (info.isType<RemoteServerTimeoutError>()) {
sendEvent(
ErrorEvent(
errorId: ErrorType.remoteServerTimeout.value,
),
id: id,
);
return;
}
if (isGroupchat) {
// The JID points to a groupchat. Handle that on the UI side
sendEvent(
JidIsGroupchatEvent(),
id: id,
);
} else {
await cs.createOrUpdateConversation(
jid,
create: () async {
// Create
final css = GetIt.I.get<ContactsService>();
final contactId = await css.getContactIdForJid(jid);
final prefs =
await GetIt.I.get<PreferencesService>().getPreferences();
final newConversation = await cs.addConversationFromData(
jid.split('@')[0],
null,
ConversationType.chat,
'',
jid,
0,
DateTime.now().millisecondsSinceEpoch,
true,
prefs.defaultMuteState,
prefs.enableOmemoByDefault,
contactId,
await css.getProfilePicturePathForJid(jid),
await css.getContactDisplayName(contactId),
);
sendEvent(
AddContactResultEvent(
conversation: newConversation,
added: !inRoster,
),
id: id,
);
return newConversation;
},
);
// Add to roster, if required
await _maybeAchieveBothSubscription(jid);
}
}
}
Future<void> performRemoveContact(
RemoveContactCommand command, {
dynamic extra,
}) async {
final rs = GetIt.I.get<RosterService>();
final cs = GetIt.I.get<ConversationService>();
// Remove from roster
await rs.removeFromRosterWrapper(command.jid);
// Update the conversation
final conversation = await cs.getConversationByJid(command.jid);
if (conversation != null) {
sendEvent(
ConversationUpdatedEvent(
conversation: conversation.copyWith(
showAddToRoster: true,
),
),
);
}
}
Future<void> performRequestDownload(
RequestDownloadCommand command, {
dynamic extra,
}) async {
final ms = GetIt.I.get<MessageService>();
final srv = GetIt.I.get<HttpFileTransferService>();
final message = await ms.updateMessage(
command.message.id,
isDownloading: true,
);
sendEvent(MessageUpdatedEvent(message: message));
final fileMetadata = command.message.fileMetadata!;
final metadata = await peekFile(fileMetadata.sourceUrls!.first);
// TODO(Unknown): Maybe deduplicate with the code in the xmpp service
// NOTE: This either works by returing "jpg" for ".../hallo.jpg" or fails
// for ".../aaaaaaaaa", in which case we would've failed anyways.
final ext = fileMetadata.sourceUrls!.first.split('.').last;
final mimeGuess = metadata.mime ?? guessMimeTypeFromExtension(ext);
await srv.downloadFile(
FileDownloadJob(
MediaFileLocation(
fileMetadata.sourceUrls!,
fileMetadata.filename,
fileMetadata.encryptionScheme,
fileMetadata.encryptionKey != null
? base64Decode(fileMetadata.encryptionKey!)
: null,
fileMetadata.encryptionIv != null
? base64Decode(fileMetadata.encryptionIv!)
: null,
fileMetadata.plaintextHashes,
fileMetadata.ciphertextHashes,
null,
),
message.id,
message.fileMetadata!.id,
message.fileMetadata!.plaintextHashes?.isNotEmpty ?? false,
message.conversationJid,
mimeGuess,
),
);
}
Future<void> performSetAvatar(SetAvatarCommand command, {dynamic extra}) async {
await GetIt.I.get<XmppStateService>().modifyXmppState(
(state) => state.copyWith(
avatarUrl: command.path,
avatarHash: command.hash,
),
);
await GetIt.I.get<AvatarService>().publishAvatar(command.path, command.hash);
}
Future<void> performSetShareOnlineStatus(
SetShareOnlineStatusCommand command, {
dynamic extra,
}) async {
final rs = GetIt.I.get<RosterService>();
final item = await rs.getRosterItemByJid(command.jid);
// TODO(Unknown): Maybe log
if (item == null) return;
final jid = JID.fromString(command.jid);
final pm = GetIt.I
.get<XmppConnection>()
.getManagerById<PresenceManager>(presenceManager)!;
if (command.share) {
switch (item.subscription) {
case 'to':
await pm.acceptSubscriptionRequest(jid);
break;
case 'none':
case 'from':
await pm.requestSubscription(jid);
break;
}
} else {
switch (item.subscription) {
case 'both':
case 'from':
case 'to':
await pm.unsubscribe(jid);
break;
}
}
}
Future<void> performCloseConversation(
CloseConversationCommand command, {
dynamic extra,
}) async {
final cs = GetIt.I.get<ConversationService>();
await cs.createOrUpdateConversation(
command.jid,
update: (c) async {
return cs.updateConversation(
command.jid,
open: false,
);
},
);
sendEvent(
CloseConversationEvent(),
id: extra as String,
);
}
Future<void> performSendChatState(
SendChatStateCommand command, {
dynamic extra,
}) async {
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
// Only send chat states if the users wants to send them
if (!prefs.sendChatMarkers) return;
// Only send chat states when we're connected
if (!(await GetIt.I.get<ConnectivityService>().hasConnection())) return;
final conn = GetIt.I.get<XmppConnection>();
if (command.jid != '') {
await conn
.getManagerById<ChatStateManager>(chatStateManager)!
.sendChatState(ChatState.fromName(command.state), command.jid);
}
}
Future<void> performGetFeatures(
GetFeaturesCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final conn = GetIt.I.get<XmppConnection>();
final sm = conn.getNegotiatorById<StreamManagementNegotiator>(
streamManagementNegotiator,
)!;
final csi = conn.getNegotiatorById<CSINegotiator>(csiNegotiator)!;
final httpFileUpload =
conn.getManagerById<HttpFileUploadManager>(httpFileUploadManager)!;
final userBlocking = conn.getManagerById<BlockingManager>(blockingManager)!;
final carbons = conn.getManagerById<CarbonsManager>(carbonsManager)!;
sendEvent(
GetFeaturesEvent(
supportsStreamManagement: sm.isSupported,
supportsCsi: csi.isSupported,
supportsHttpFileUpload: await httpFileUpload.isSupported(),
supportsUserBlocking: await userBlocking.isSupported(),
supportsCarbons: await carbons.isSupported(),
),
id: id,
);
}
Future<void> performSignOut(SignOutCommand command, {dynamic extra}) async {
final id = extra as String;
final conn = GetIt.I.get<XmppConnection>();
final xss = GetIt.I.get<XmppStateService>();
unawaited(conn.disconnect());
await xss.modifyXmppState(
(state) => XmppState(),
);
sendEvent(
SignedOutEvent(),
id: id,
);
}
Future<void> performSendFiles(SendFilesCommand command, {dynamic extra}) async {
await GetIt.I.get<XmppService>().sendFiles(command.paths, command.recipients);
}
Future<void> performSetMuteState(
SetConversationMuteStatusCommand command, {
dynamic extra,
}) async {
final cs = GetIt.I.get<ConversationService>();
final conversation = await cs.createOrUpdateConversation(
command.jid,
update: (c) async {
return cs.updateConversation(
command.jid,
muted: command.muted,
);
},
);
if (conversation != null) {
sendEvent(ConversationUpdatedEvent(conversation: conversation));
}
}
Future<void> performGetOmemoFingerprints(
GetConversationOmemoFingerprintsCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final omemo = GetIt.I.get<OmemoService>();
sendEvent(
GetConversationOmemoFingerprintsResult(
fingerprints: await omemo.getFingerprintsForJid(command.jid),
),
id: id,
);
}
Future<void> performEnableOmemoKey(
SetOmemoDeviceEnabledCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final omemo = GetIt.I.get<OmemoService>();
await omemo.setDeviceEnablement(
command.jid,
command.deviceId,
command.enabled,
);
await performGetOmemoFingerprints(
GetConversationOmemoFingerprintsCommand(jid: command.jid),
extra: id,
);
}
Future<void> performRecreateSessions(
RecreateSessionsCommand command, {
dynamic extra,
}) async {
// Remove all ratchets
await GetIt.I.get<OmemoService>().removeAllRatchets(command.jid);
// And force the creation of new ones
await GetIt.I
.get<XmppConnection>()
.getManagerById<OmemoManager>(omemoManager)!
.sendOmemoHeartbeat(
command.jid,
);
}
Future<void> performSetOmemoEnabled(
SetOmemoEnabledCommand command, {
dynamic extra,
}) async {
final cs = GetIt.I.get<ConversationService>();
await cs.createOrUpdateConversation(
command.jid,
update: (c) async {
return cs.updateConversation(
command.jid,
encrypted: command.enabled,
);
},
);
}
Future<void> performGetOwnOmemoFingerprints(
GetOwnOmemoFingerprintsCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final os = GetIt.I.get<OmemoService>();
final xs = GetIt.I.get<XmppService>();
final jid = (await xs.getConnectionSettings())!.jid;
final device = await os.getDevice();
sendEvent(
GetOwnOmemoFingerprintsResult(
ownDeviceFingerprint: await device.getFingerprint(),
ownDeviceId: device.id,
fingerprints: await os.getFingerprintsForJid(jid.toString()),
),
id: id,
);
}
Future<void> performRemoveOwnDevice(
RemoveOwnDeviceCommand command, {
dynamic extra,
}) async {
await GetIt.I
.get<XmppConnection>()
.getManagerById<OmemoManager>(omemoManager)!
.deleteDevice(command.deviceId);
}
Future<void> performRegenerateOwnDevice(
RegenerateOwnDeviceCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final device = await GetIt.I.get<OmemoService>().regenerateDevice();
sendEvent(
RegenerateOwnDeviceResult(device: device),
id: id,
);
}
Future<void> performMessageRetraction(
RetractMessageCommentCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<MessageService>().retractMessage(
command.conversationJid,
command.originId,
'',
true,
);
if (command.conversationJid != '') {
final manager = GetIt.I
.get<XmppConnection>()
.getManagerById<MessageManager>(messageManager)!;
await manager.sendMessage(
JID.fromString(command.conversationJid),
TypedMap<StanzaHandlerExtension>.fromList([
MessageRetractionData(command.originId, t.messages.retractedFallback),
]),
);
}
}
Future<void> performMarkConversationAsRead(
MarkConversationAsReadCommand command, {
dynamic extra,
}) async {
final cs = GetIt.I.get<ConversationService>();
// Update the database
final conversation = await cs.createOrUpdateConversation(
command.conversationJid,
update: (c) async {
return cs.updateConversation(
command.conversationJid,
unreadCounter: 0,
);
},
);
if (conversation != null) {
sendEvent(ConversationUpdatedEvent(conversation: conversation));
if (conversation.lastMessage != null) {
await GetIt.I.get<MessageService>().markMessageAsRead(
conversation.lastMessage!.id,
conversation.type != ConversationType.note,
);
}
}
// Dismiss notifications for that chat
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
command.conversationJid,
);
}
Future<void> performMarkMessageAsRead(
MarkMessageAsReadCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<MessageService>().markMessageAsRead(
command.id,
command.sendMarker,
);
}
Future<void> performAddMessageReaction(
AddReactionToMessageCommand command, {
dynamic extra,
}) async {
final rs = GetIt.I.get<ReactionsService>();
final msg = await rs.addNewReaction(
command.messageId,
command.conversationJid,
command.emoji,
);
if (msg == null) {
return;
}
if (command.conversationJid != '') {
final jid = (await GetIt.I.get<XmppStateService>().getXmppState()).jid!;
// Send the reaction
final manager = GetIt.I
.get<XmppConnection>()
.getManagerById<MessageManager>(messageManager)!;
await manager.sendMessage(
JID.fromString(command.conversationJid),
TypedMap<StanzaHandlerExtension>.fromList([
MessageReactionsData(
msg.originId ?? msg.sid,
await rs.getReactionsForMessageByJid(
command.messageId,
jid,
),
),
const MarkableData(false),
MessageProcessingHintData([
if (!msg.containsNoStore) MessageProcessingHint.store,
]),
]),
);
}
}
Future<void> performRemoveMessageReaction(
RemoveReactionFromMessageCommand command, {
dynamic extra,
}) async {
final rs = GetIt.I.get<ReactionsService>();
final msg = await rs.removeReaction(
command.messageId,
command.conversationJid,
command.emoji,
);
if (msg == null) {
return;
}
if (command.conversationJid != '') {
final jid = (await GetIt.I.get<XmppStateService>().getXmppState()).jid!;
// Send the reaction
final manager = GetIt.I
.get<XmppConnection>()
.getManagerById<MessageManager>(messageManager)!;
await manager.sendMessage(
JID.fromString(command.conversationJid),
TypedMap<StanzaHandlerExtension>.fromList([
MessageReactionsData(
msg.originId ?? msg.sid,
await rs.getReactionsForMessageByJid(
command.messageId,
jid,
),
),
const MarkableData(false),
MessageProcessingHintData([
if (!msg.containsNoStore) MessageProcessingHint.store,
]),
]),
);
}
}
Future<void> performMarkDeviceVerified(
MarkOmemoDeviceAsVerifiedCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<OmemoService>().setDeviceVerified(
command.jid,
command.deviceId,
);
}
Future<void> performImportStickerPack(
ImportStickerPackCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final result =
await GetIt.I.get<StickersService>().importFromFile(command.path);
if (result != null) {
sendEvent(
StickerPackImportSuccessEvent(
stickerPack: result,
),
id: id,
);
} else {
sendEvent(
StickerPackImportFailureEvent(),
id: id,
);
}
}
Future<void> performSendSticker(
SendStickerCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<XmppService>().sendMessage(
body: command.sticker.desc,
recipients: [command.recipient],
sticker: command.sticker,
currentConversationJid: command.recipient,
quotedMessage: command.quotes,
);
}
Future<void> performRemoveStickerPack(
RemoveStickerPackCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<StickersService>().removeStickerPack(
command.stickerPackId,
);
}
Future<void> performFetchStickerPack(
FetchStickerPackCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final result = await GetIt.I
.get<XmppConnection>()
.getManagerById<StickersManager>(stickersManager)!
.fetchStickerPack(JID.fromString(command.jid), command.stickerPackId);
if (result.isType<PubSubError>()) {
sendEvent(
FetchStickerPackFailureResult(),
id: id,
);
} else {
final stickerPack = result.get<StickerPack>();
sendEvent(
FetchStickerPackSuccessResult(
stickerPack: sticker_pack.StickerPack(
command.stickerPackId,
stickerPack.name,
stickerPack.summary,
stickerPack.stickers
.map(
(s) => sticker.Sticker(
'',
command.stickerPackId,
s.metadata.desc!,
s.suggests,
FileMetadata(
'',
null,
s.sources
.whereType<StatelessFileSharingUrlSource>()
.map((src) => src.url)
.toList(),
s.metadata.mediaType,
s.metadata.size,
// TODO(Unknown): One day
null,
null,
s.metadata.width,
s.metadata.height,
s.metadata.hashes,
null,
null,
null,
null,
s.metadata.name ?? '',
),
),
)
.toList(),
stickerPack.hashAlgorithm.toName(),
stickerPack.hashValue,
stickerPack.restricted,
false,
0,
0,
),
),
id: id,
);
}
}
Future<void> performStickerPackInstall(
InstallStickerPackCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final ss = GetIt.I.get<StickersService>();
final pack = await ss.installFromPubSub(command.stickerPack);
if (pack != null) {
sendEvent(
StickerPackInstallSuccessEvent(
stickerPack: pack,
),
id: id,
);
} else {
sendEvent(
StickerPackInstallFailureEvent(),
id: id,
);
}
}
Future<void> performGetBlocklist(
GetBlocklistCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final result = await GetIt.I.get<BlocklistService>().getBlocklist();
sendEvent(
GetBlocklistResultEvent(
entries: result,
),
id: id,
);
}
Future<void> performGetPagedMessages(
GetPagedMessagesCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final result = await GetIt.I.get<MessageService>().getPaginatedMessagesForJid(
command.conversationJid,
command.olderThan,
command.timestamp,
);
sendEvent(
PagedMessagesResultEvent(
messages: result,
),
id: id,
);
}
Future<void> performGetPagedSharedMedia(
GetPagedSharedMediaCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final result =
await GetIt.I.get<MessageService>().getPaginatedSharedMediaMessagesForJid(
command.conversationJid,
command.olderThan,
command.timestamp,
);
sendEvent(
PagedMessagesResultEvent(
messages: result,
),
id: id,
);
}
Future<void> performGetReactions(
GetReactionsForMessageCommand command, {
dynamic extra,
}) async {
final id = extra as String;
final reactionsRaw =
await GetIt.I.get<ReactionsService>().getReactionsForMessage(
command.messageId,
);
final reactionsMap = <String, List<String>>{};
for (final reaction in reactionsRaw) {
if (reactionsMap.containsKey(reaction.senderJid)) {
reactionsMap[reaction.senderJid]!.add(reaction.emoji);
} else {
reactionsMap[reaction.senderJid] = List<String>.from([reaction.emoji]);
}
}
sendEvent(
ReactionsForMessageResult(
reactions: reactionsMap.entries
.map(
(entry) => ReactionGroup(
entry.key,
entry.value,
),
)
.toList(),
),
id: id,
);
}
Future<void> performRequestAvatarForJid(
RequestAvatarForJidCommand command, {
dynamic extra,
}) async {
final as = GetIt.I.get<AvatarService>();
Future<void> future;
if (command.ownAvatar) {
future = as.requestOwnAvatar();
} else {
future = as.requestAvatar(
JID.fromString(command.jid),
command.hash,
);
}
unawaited(future);
}
Future<void> performGetStorageUsage(
GetStorageUsageCommand command, {
dynamic extra,
}) async {
sendEvent(
GetStorageUsageEvent(
mediaUsage: await GetIt.I.get<StorageService>().computeUsedMediaStorage(),
stickerUsage:
await GetIt.I.get<StorageService>().computeUsedStickerStorage(),
),
id: extra as String,
);
}
Future<void> performOldMediaFileDeletion(
DeleteOldMediaFilesCommand command, {
dynamic extra,
}) async {
await GetIt.I.get<StorageService>().deleteOldMediaFiles(command.timeOffset);
sendEvent(
DeleteOldMediaFilesDoneEvent(
newUsage: await GetIt.I.get<StorageService>().computeUsedMediaStorage(),
conversations:
(await GetIt.I.get<ConversationService>().loadConversations())
.where((c) => c.open)
.toList(),
),
id: extra as String,
);
}
Future<void> performGetPagedStickerPacks(
GetPagedStickerPackCommand command, {
dynamic extra,
}) async {
final result = await GetIt.I.get<StickersService>().getPaginatedStickerPacks(
command.olderThan,
command.timestamp,
command.includeStickers,
);
sendEvent(
PagedStickerPackResult(
stickerPacks: result,
),
id: extra as String,
);
}
Future<void> performGetStickerPackById(
GetStickerPackByIdCommand command, {
dynamic extra,
}) async {
sendEvent(
GetStickerPackByIdResult(
stickerPack: await GetIt.I.get<StickersService>().getStickerPackById(
command.id,
),
),
id: extra as String,
);
}
Future<void> performDebugCommand(
DebugCommand command, {
dynamic extra,
}) async {
final conn = GetIt.I.get<XmppConnection>();
if (command.id == debug.DebugCommand.clearStreamResumption.id) {
// Disconnect
await conn.disconnect();
// Reset stream management
await conn.getManagerById<StreamManagementManager>(smManager)!.resetState();
// Reconnect
await conn.connect(
shouldReconnect: true,
waitForConnection: true,
);
} else if (command.id == debug.DebugCommand.requestRoster.id) {
await conn
.getManagerById<RosterManager>(rosterManager)!
.requestRoster(useRosterVersion: false);
} else if (command.id == debug.DebugCommand.logAvailableMediaFiles.id) {
final db = GetIt.I.get<DatabaseService>().database;
final results = await db.rawQuery(
'''
SELECT
path,
id
FROM
$fileMetadataTable AS fmt
WHERE
AND NOT EXISTS (SELECT id from $stickersTable WHERE file_metadata_id = fmt.id)
AND path IS NOT NULL
''',
);
Logger.root.finest(results);
}
}