moxxy/lib/service/message.dart

318 lines
9.7 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/models/media.dart';
import 'package:moxxyv2/shared/models/message.dart';
class MessageService {
// TODO(PapaTutuWawa): Maybe remove the cache
final HashMap<String, List<Message>> _messageCache = HashMap();
/// Logger
final Logger _log = Logger('MessageService');
/// Returns the messages for [jid], either from cache or from the database.
Future<List<Message>> getMessagesForJid(String jid) async {
if (!_messageCache.containsKey(jid)) {
_messageCache[jid] = await GetIt.I.get<DatabaseService>().loadMessagesForJid(jid);
}
final messages = _messageCache[jid];
if (messages == null) {
_log.warning('No messages found for $jid. Returning [].');
return [];
}
return messages;
}
Future<List<Message>> getPaginatedMessagesForJid(String jid, int? oldestTimestamp) async {
return GetIt.I.get<DatabaseService>().getPaginatedMessagesForJid(
jid,
oldestTimestamp,
);
}
/// Wrapper around [DatabaseService]'s addMessageFromData that updates the cache.
Future<Message> addMessageFromData(
String body,
int timestamp,
String sender,
String conversationJid,
bool isMedia,
String sid,
bool isFileUploadNotification,
bool encrypted,
bool containsNoStore,
{
String? srcUrl,
String? key,
String? iv,
String? encryptionScheme,
String? mediaUrl,
String? mediaType,
String? thumbnailData,
int? mediaWidth,
int? mediaHeight,
String? originId,
String? quoteId,
String? filename,
int? errorType,
int? warningType,
Map<String, String>? plaintextHashes,
Map<String, String>? ciphertextHashes,
bool isDownloading = false,
bool isUploading = false,
int? mediaSize,
String? stickerPackId,
String? stickerHashKey,
int? pseudoMessageType,
Map<String, dynamic>? pseudoMessageData,
}
) async {
final msg = await GetIt.I.get<DatabaseService>().addMessageFromData(
body,
timestamp,
sender,
conversationJid,
isMedia,
sid,
isFileUploadNotification,
encrypted,
containsNoStore,
srcUrl: srcUrl,
key: key,
iv: iv,
encryptionScheme: encryptionScheme,
mediaUrl: mediaUrl,
mediaType: mediaType,
thumbnailData: thumbnailData,
mediaWidth: mediaWidth,
mediaHeight: mediaHeight,
originId: originId,
quoteId: quoteId,
filename: filename,
errorType: errorType,
warningType: warningType,
plaintextHashes: plaintextHashes,
ciphertextHashes: ciphertextHashes,
isUploading: isUploading,
isDownloading: isDownloading,
mediaSize: mediaSize,
stickerPackId: stickerPackId,
stickerHashKey: stickerHashKey,
pseudoMessageType: pseudoMessageType,
pseudoMessageData: pseudoMessageData,
);
// Only update the cache if the conversation already has been loaded. This prevents
// us from accidentally not loading the conversation afterwards.
if (_messageCache.containsKey(conversationJid)) {
_messageCache[conversationJid] = _messageCache[conversationJid]!..add(msg);
}
return msg;
}
Future<Message?> getMessageByStanzaId(String conversationJid, String stanzaId) async {
if (!_messageCache.containsKey(conversationJid)) {
await getMessagesForJid(conversationJid);
}
return firstWhereOrNull(
_messageCache[conversationJid]!,
(message) => message.sid == stanzaId,
);
}
Future<Message?> getMessageByStanzaOrOriginId(String conversationJid, String id) async {
if (!_messageCache.containsKey(conversationJid)) {
await getMessagesForJid(conversationJid);
}
return firstWhereOrNull(
_messageCache[conversationJid]!,
(message) => message.sid == id || message.originId == id,
);
}
Future<Message?> getMessageById(String conversationJid, int id) async {
if (!_messageCache.containsKey(conversationJid)) {
await getMessagesForJid(conversationJid);
}
return firstWhereOrNull(
_messageCache[conversationJid]!,
(message) => message.id == id,
);
}
/// Wrapper around [DatabaseService]'s updateMessage that updates the cache
Future<Message> updateMessage(int id, {
Object? body = notSpecified,
Object? mediaUrl = notSpecified,
Object? mediaType = notSpecified,
bool? isMedia,
bool? received,
bool? displayed,
bool? acked,
Object? errorType = notSpecified,
Object? warningType = notSpecified,
bool? isFileUploadNotification,
Object? srcUrl = notSpecified,
Object? key = notSpecified,
Object? iv = notSpecified,
Object? encryptionScheme = notSpecified,
Object? mediaWidth = notSpecified,
Object? mediaHeight = notSpecified,
Object? mediaSize = notSpecified,
bool? isUploading,
bool? isDownloading,
Object? originId = notSpecified,
Object? sid = notSpecified,
Object? thumbnailData = notSpecified,
bool? isRetracted,
bool? isEdited,
Object? reactions = notSpecified,
}) async {
final newMessage = await GetIt.I.get<DatabaseService>().updateMessage(
id,
body: body,
mediaUrl: mediaUrl,
mediaType: mediaType,
received: received,
displayed: displayed,
acked: acked,
errorType: errorType,
warningType: warningType,
isFileUploadNotification: isFileUploadNotification,
srcUrl: srcUrl,
key: key,
iv: iv,
encryptionScheme: encryptionScheme,
mediaWidth: mediaWidth,
mediaHeight: mediaHeight,
mediaSize: mediaSize,
isUploading: isUploading,
isDownloading: isDownloading,
originId: originId,
sid: sid,
isRetracted: isRetracted,
isMedia: isMedia,
thumbnailData: thumbnailData,
isEdited: isEdited,
reactions: reactions,
);
if (_messageCache.containsKey(newMessage.conversationJid)) {
_messageCache[newMessage.conversationJid] = _messageCache[newMessage.conversationJid]!.map((m) {
if (m.id == newMessage.id) return newMessage;
return m;
}).toList();
}
return newMessage;
}
/// Helper function that manages everything related to retracting a message. It
/// - Replaces all metadata of the message with null values and marks it as retracted
/// - Modified the conversation, if the retracted message was the newest message
/// - Remove the SharedMedium from the database, if one referenced the retracted message
/// - Update the UI
///
/// [conversationJid] is the bare JID of the conversation this message belongs to.
/// [originId] is the origin Id of the message that is to be retracted.
/// [bareSender] is the bare JID of the sender of the retraction message.
/// [selfRetract] indicates whether the message retraction came from the UI. If true,
/// then the sender check (see security considerations of XEP-0424) is skipped as
/// the UI already verifies it.
Future<void> retractMessage(String conversationJid, String originId, String bareSender, bool selfRetract) async {
final msg = await GetIt.I.get<DatabaseService>().getMessageByOriginId(
originId,
conversationJid,
);
if (msg == null) {
_log.finest('Got message retraction for origin Id $originId, but did not find the message');
return;
}
// Check if the retraction was sent by the original sender
if (!selfRetract) {
if (JID.fromString(msg.sender).toBare().toString() != bareSender) {
_log.warning('Received invalid message retraction from $bareSender but its original sender is ${msg.sender}');
return;
}
}
final isMedia = msg.isMedia;
final mediaUrl = msg.mediaUrl;
final retractedMessage = await updateMessage(
msg.id,
isMedia: false,
mediaUrl: null,
mediaType: null,
warningType: null,
errorType: null,
srcUrl: null,
key: null,
iv: null,
encryptionScheme: null,
mediaWidth: null,
mediaHeight: null,
mediaSize: null,
isRetracted: true,
thumbnailData: null,
body: '',
);
sendEvent(MessageUpdatedEvent(message: retractedMessage));
final cs = GetIt.I.get<ConversationService>();
final conversation = await cs.getConversationByJid(conversationJid);
if (conversation != null) {
if (conversation.lastMessage?.id == msg.id) {
var newConversation = conversation.copyWith(
lastMessage: retractedMessage,
);
if (isMedia) {
await GetIt.I.get<DatabaseService>().removeSharedMediumByMessageId(msg.id);
newConversation = newConversation.copyWith(
sharedMedia: newConversation.sharedMedia.where((SharedMedium medium) {
return medium.messageId != msg.id;
}).toList(),
);
// Delete the file if we downloaded it
if (mediaUrl != null) {
final file = File(mediaUrl);
if (file.existsSync()) {
unawaited(file.delete());
}
}
}
cs.setConversation(newConversation);
sendEvent(
ConversationUpdatedEvent(
conversation: newConversation,
),
);
}
} else {
_log.warning('Failed to find conversation with conversationJid $conversationJid');
}
}
}