moxxy/lib/service/events.dart

650 lines
21 KiB
Dart

import 'dart:async';
import 'dart:convert';
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/conversation.dart';
import 'package:moxxyv2/service/database/database.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/moxxmpp/reconnect.dart';
import 'package:moxxyv2/service/notifications.dart';
import 'package:moxxyv2/service/omemo/omemo.dart';
import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/service/roster.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/service/state.dart';
import 'package:moxxyv2/service/xmpp.dart';
import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/eventhandler.dart';
import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/preferences.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<GetMessagesForJidCommand>(performGetMessagesForJid),
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),
]);
GetIt.I.registerSingleton<EventHandler>(handler);
}
Future<void> performLogin(LoginCommand command, { dynamic extra }) async {
final id = extra as String;
GetIt.I.get<Logger>().fine('Performing login...');
GetIt.I.get<MoxxyReconnectionPolicy>().setShouldReconnect(false);
final result = await GetIt.I.get<XmppService>().connectAwaitable(
ConnectionSettings(
jid: JID.fromString(command.jid),
password: command.password,
useDirectTLS: command.useDirectTLS,
allowPlainAuth: true,
),
true,
);
GetIt.I.get<Logger>().fine('Login done');
// ignore: avoid_dynamic_calls
if (result.success) {
final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
GetIt.I.get<MoxxyReconnectionPolicy>().setShouldReconnect(true);
final settings = GetIt.I.get<XmppConnection>().getConnectionSettings();
sendEvent(
LoginSuccessfulEvent(
jid: settings.jid.toString(),
preStart: await _buildPreStartDoneEvent(preferences),
),
id:id,
);
} else {
GetIt.I.get<MoxxyReconnectionPolicy>().setShouldReconnect(false);
sendEvent(
LoginFailureEvent(
reason: xmppErrorToTranslatableString(result.error!),
),
id: id,
);
}
}
Future<PreStartDoneEvent> _buildPreStartDoneEvent(PreferencesState preferences) async {
final xmpp = GetIt.I.get<XmppService>();
final state = await xmpp.getXmppState();
await GetIt.I.get<RosterService>().loadRosterFromDatabase();
// Check some permissions
final storagePerm = await Permission.storage.status;
final permissions = List<int>.empty(growable: true);
if (storagePerm.isDenied /*&& !state.askedStoragePermission*/) {
permissions.add(Permission.storage.value);
await xmpp.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: permissions,
preferences: preferences,
conversations: (await GetIt.I.get<DatabaseService>().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;
// Prevent a race condition where the UI sends the prestart command before the service
// has finished setting everything up
GetIt.I.get<Logger>().finest('Waiting for preStart future to complete..');
await GetIt.I.get<Completer<void>>().future;
GetIt.I.get<Logger>().finest('PreStart future done');
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 conversation = await cs.getConversationByJid(command.jid);
if (conversation != null) {
if (!conversation.open) {
// Re-open the conversation
final updatedConversation = await cs.updateConversation(
conversation.id,
open: true,
);
sendEvent(
ConversationAddedEvent(
conversation: updatedConversation,
),
id: id,
);
return;
}
sendEvent(
NoConversationModifiedEvent(),
id: id,
);
return;
} else {
final conversation = await cs.addConversationFromData(
command.title,
-1,
false,
command.lastMessageBody,
command.avatarUrl,
command.jid,
0,
-1,
true,
// TODO(PapaTutuWawa): Take as an argument
false,
(await GetIt.I.get<PreferencesService>().getPreferences()).enableOmemoByDefault,
);
sendEvent(
ConversationAddedEvent(
conversation: conversation,
),
id: id,
);
}
}
Future<void> performGetMessagesForJid(GetMessagesForJidCommand command, { dynamic extra }) async {
final id = extra as String;
sendEvent(
MessagesResultEvent(
messages: await GetIt.I.get<MessageService>().getMessagesForJid(command.jid),
),
id: id,
);
}
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
if (command.jid != null) {
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(command.jid!);
}
}
Future<void> performSendMessage(SendMessageCommand command, { dynamic extra }) async {
await GetIt.I.get<XmppService>().sendMessage(
body: command.body,
recipients: command.recipients,
chatState: command.chatState.isNotEmpty
? chatStateFromString(command.chatState)
: null,
quotedMessage: command.quotedMessage,
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 {
await GetIt.I.get<PreferencesService>().modifyPreferences((_) => command.preferences);
// Set the logging mode
if (!kDebugMode) {
final enableDebug = command.preferences.debugEnabled;
Logger.root.level = enableDebug ? Level.ALL : Level.INFO;
}
// 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(),
);
}
Future<void> performAddContact(AddContactCommand command, { dynamic extra }) async {
final id = extra as String;
final jid = command.jid;
final roster = GetIt.I.get<RosterService>();
if (await roster.isInRoster(jid)) {
sendEvent(AddContactResultEvent(added: false), id: id);
return;
}
final cs = GetIt.I.get<ConversationService>();
final conversation = await cs.getConversationByJid(jid);
if (conversation != null) {
final c = await cs.updateConversation(
conversation.id,
open: true,
);
sendEvent(
AddContactResultEvent(conversation: c, added: false),
id: id,
);
} else {
final c = await cs.addConversationFromData(
jid.split('@')[0],
-1,
false,
'',
'',
jid,
0,
-1,
true,
// TODO(PapaTutuWawa): Take as an argument
false,
(await GetIt.I.get<PreferencesService>().getPreferences()).enableOmemoByDefault,
);
sendEvent(
AddContactResultEvent(conversation: c, added: true),
id: id,
);
}
await roster.addToRosterWrapper('', '', jid, jid.split('@')[0]);
// Try to figure out an avatar
await GetIt.I.get<AvatarService>().subscribeJid(jid);
await GetIt.I.get<AvatarService>().fetchAndUpdateAvatarForJid(jid, '');
}
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 metadata = await peekFile(command.message.srcUrl!);
// 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 = message.srcUrl!.split('.').last;
final mimeGuess = metadata.mime ?? guessMimeTypeFromExtension(ext);
await srv.downloadFile(
FileDownloadJob(
MediaFileLocation(
message.srcUrl!,
message.filename ?? filenameFromUrl(message.srcUrl!),
message.encryptionScheme,
message.key != null ? base64Decode(message.key!) : null,
message.iv != null ? base64Decode(message.iv!) : null,
message.plaintextHashes,
message.ciphertextHashes,
),
message.id,
message.conversationJid,
mimeGuess,
),
);
}
Future<void> performSetAvatar(SetAvatarCommand command, { dynamic extra }) async {
await GetIt.I.get<XmppService>().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 roster = GetIt.I.get<RosterService>();
final rs = GetIt.I.get<RosterService>();
final item = await rs.getRosterItemByJid(command.jid);
// TODO(Unknown): Maybe log
if (item == null) return;
if (command.share) {
if (item.ask == 'subscribe') {
await roster.acceptSubscriptionRequest(command.jid);
} else {
roster.sendSubscriptionRequest(command.jid);
}
} else {
if (item.ask == 'subscribe') {
await roster.rejectSubscriptionRequest(command.jid);
} else {
roster.sendUnsubscriptionRequest(command.jid);
}
}
}
Future<void> performCloseConversation(CloseConversationCommand command, { dynamic extra }) async {
final cs = GetIt.I.get<ConversationService>();
final conversation = await cs.getConversationByJid(command.jid);
if (conversation == null) {
// TODO(Unknown): Should not happen
return;
}
await cs.updateConversation(
conversation.id,
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;
final conn = GetIt.I.get<XmppConnection>();
conn
.getManagerById<ChatStateManager>(chatStateManager)!
.sendChatState(chatStateFromString(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)!;
sendEvent(
GetFeaturesEvent(
supportsStreamManagement: sm.isSupported,
supportsCsi: csi.isSupported,
supportsHttpFileUpload: await httpFileUpload.isSupported(),
supportsUserBlocking: await userBlocking.isSupported(),
),
id: id,
);
}
Future<void> performSignOut(SignOutCommand command, { dynamic extra }) async {
final id = extra as String;
final conn = GetIt.I.get<XmppConnection>();
final xmpp = GetIt.I.get<XmppService>();
await conn.disconnect();
await xmpp.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.getConversationByJid(command.jid);
final newConversation = await cs.updateConversation(
conversation!.id,
muted: command.muted,
);
sendEvent(ConversationUpdatedEvent(conversation: newConversation));
}
Future<void> performGetOmemoFingerprints(GetConversationOmemoFingerprintsCommand command, { dynamic extra }) async {
final id = extra as String;
final omemo = GetIt.I.get<OmemoService>();
sendEvent(
GetConversationOmemoFingerprintsResult(
fingerprints: await omemo.getOmemoKeysForJid(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.setOmemoKeyEnabled(command.jid, command.deviceId, command.enabled);
await performGetOmemoFingerprints(
GetConversationOmemoFingerprintsCommand(jid: command.jid),
extra: id,
);
}
Future<void> performRecreateSessions(RecreateSessionsCommand command, { dynamic extra }) async {
await GetIt.I.get<OmemoService>().removeAllSessions(command.jid);
final conn = GetIt.I.get<XmppConnection>();
await conn.getManagerById<OmemoManager>(omemoManager)!.sendEmptyMessage(
JID.fromString(command.jid),
findNewSessions: true,
);
}
Future<void> performSetOmemoEnabled(SetOmemoEnabledCommand command, { dynamic extra }) async {
final cs = GetIt.I.get<ConversationService>();
final conversation = await cs.getConversationByJid(command.jid);
await cs.updateConversation(
conversation!.id,
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>();
await os.ensureInitialized();
final jid = (await xs.getConnectionSettings())!.jid;
sendEvent(
GetOwnOmemoFingerprintsResult(
ownDeviceFingerprint: await os.getDeviceFingerprint(),
ownDeviceId: await os.getDeviceId(),
fingerprints: await os.getOwnFingerprints(jid),
),
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 jid = GetIt.I.get<XmppConnection>()
.getConnectionSettings()
.jid.toBare()
.toString();
final device = await GetIt.I.get<OmemoService>().regenerateDevice(jid);
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,
);
// Send the retraction
(GetIt.I.get<XmppConnection>().getManagerById(messageManager)! as MessageManager)
.sendMessage(
MessageDetails(
to: command.conversationJid,
messageRetraction: MessageRetractionData(
command.originId,
t.messages.retractedFallback,
),
),
);
}
Future<void> performMarkConversationAsRead(MarkConversationAsReadCommand command, { dynamic extra }) async {
// Update the database
final conversation = await GetIt.I.get<ConversationService>().updateConversation(
command.conversationId,
unreadCounter: 0,
);
// Dismiss notifications for that chat
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(
conversation.jid,
);
sendEvent(ConversationUpdatedEvent(conversation: conversation));
final msg = await GetIt.I.get<MessageService>().getMessageById(
conversation.jid,
conversation.lastMessageId,
);
if (msg != null) {
await GetIt.I.get<XmppService>().sendReadMarker(
conversation.jid,
msg.sid,
);
}
}
Future<void> performMarkMessageAsRead(MarkMessageAsReadCommand command, { dynamic extra }) async {
final cs = GetIt.I.get<ConversationService>();
final oldConversation = await cs.getConversationByJid(command.conversationJid);
final conversation = await cs.updateConversation(
oldConversation!.id,
unreadCounter: command.newUnreadCounter,
);
sendEvent(ConversationUpdatedEvent(conversation: conversation));
await GetIt.I.get<XmppService>().sendReadMarker(
command.conversationJid,
command.sid,
);
}