318 lines
9.7 KiB
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');
|
|
}
|
|
}
|
|
}
|