feat(service): Handle message retraction

This commit is contained in:
PapaTutuWawa 2022-11-20 17:30:32 +01:00
parent ef108f2e4a
commit 2dd9847566
11 changed files with 160 additions and 62 deletions

View File

@ -51,6 +51,7 @@ Future<void> createDatabase(Database db, int version) async {
isDownloading INTEGER NOT NULL,
isUploading INTEGER NOT NULL,
mediaSize INTEGER,
isRetracted INTEGER,
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messsagesTable (id)
)''',
);

View File

@ -9,7 +9,9 @@ import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/creation.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/database/migrations/0000_language.dart';
import 'package:moxxyv2/service/database/migrations/0000_retraction.dart';
import 'package:moxxyv2/service/database/migrations/0000_xmpp_state.dart';
import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/service/omemo/omemo.dart';
import 'package:moxxyv2/service/roster.dart';
import 'package:moxxyv2/service/state.dart';
@ -26,7 +28,6 @@ import 'package:sqflite_sqlcipher/sqflite.dart';
const databasePasswordKey = 'database_encryption_password';
class DatabaseService {
DatabaseService() : _log = Logger('DatabaseService');
late Database _db;
final FlutterSecureStorage _storage = const FlutterSecureStorage(
@ -55,7 +56,7 @@ class DatabaseService {
_db = await openDatabase(
dbPath,
password: key,
version: 3,
version: 4,
onCreate: createDatabase,
onConfigure: configureDatabase,
onUpgrade: (db, oldVersion, newVersion) async {
@ -67,6 +68,10 @@ class DatabaseService {
_log.finest('Running migration for database version 3');
await upgradeFromV2ToV3(db);
}
if (oldVersion < 4) {
_log.finest('Running migration for database version 4');
await upgradeFromV3ToV4(db);
}
},
);
@ -357,28 +362,45 @@ class DatabaseService {
final msg = messagesRaw.first;
return Message.fromDatabaseJson(msg, null);
}
Future<Message?> getMessageByOriginId(String id, String conversationJid) async {
final messagesRaw = await _db.query(
'Messages',
where: 'conversationJid = ? AND originId = ?',
whereArgs: [conversationJid, id],
limit: 1,
);
if (messagesRaw.isEmpty) return null;
// TODO(PapaTutuWawa): Load the quoted message
final msg = messagesRaw.first;
return Message.fromDatabaseJson(msg, null);
}
/// Updates the message item with id [id] inside the database.
Future<Message> updateMessage(int id, {
String? mediaUrl,
String? mediaType,
Object? body = notSpecified,
Object? mediaUrl = notSpecified,
Object? mediaType = notSpecified,
bool? received,
bool? displayed,
bool? acked,
int? errorType,
int? warningType,
Object? errorType = notSpecified,
Object? warningType = notSpecified,
bool? isFileUploadNotification,
String? srcUrl,
String? key,
String? iv,
String? encryptionScheme,
int? mediaWidth,
int? mediaHeight,
Object? srcUrl = notSpecified,
Object? key = notSpecified,
Object? iv = notSpecified,
Object? encryptionScheme = notSpecified,
Object? mediaWidth = notSpecified,
Object? mediaHeight = notSpecified,
bool? isDownloading,
bool? isUploading,
int? mediaSize,
String? originId,
String? sid,
Object? mediaSize = notSpecified,
Object? originId = notSpecified,
Object? sid = notSpecified,
bool? isRetracted,
}) async {
final md = (await _db.query(
'Messages',
@ -388,11 +410,11 @@ class DatabaseService {
)).first;
final m = Map<String, dynamic>.from(md);
if (mediaUrl != null) {
m['mediaUrl'] = mediaUrl;
if (mediaUrl != notSpecified) {
m['mediaUrl'] = mediaUrl as String?;
}
if (mediaType != null) {
m['mediaType'] = mediaType;
if (mediaType != notSpecified) {
m['mediaType'] = mediaType as String?;
}
if (received != null) {
m['received'] = boolToInt(received);
@ -403,35 +425,35 @@ class DatabaseService {
if (acked != null) {
m['acked'] = boolToInt(acked);
}
if (errorType != null) {
m['errorType'] = errorType;
if (errorType != notSpecified) {
m['errorType'] = errorType as int?;
}
if (warningType != null) {
m['warningType'] = warningType;
if (warningType != notSpecified) {
m['warningType'] = warningType as int?;
}
if (isFileUploadNotification != null) {
m['isFileUploadNotification'] = boolToInt(isFileUploadNotification);
}
if (srcUrl != null) {
m['srcUrl'] = srcUrl;
if (srcUrl != notSpecified) {
m['srcUrl'] = srcUrl as String?;
}
if (mediaWidth != null) {
m['mediaWidth'] = mediaWidth;
if (mediaWidth != notSpecified) {
m['mediaWidth'] = mediaWidth as int?;
}
if (mediaHeight != null) {
m['mediaHeight'] = mediaHeight;
if (mediaHeight != notSpecified) {
m['mediaHeight'] = mediaHeight as int?;
}
if (mediaSize != null) {
m['mediaSize'] = mediaSize;
if (mediaSize != notSpecified) {
m['mediaSize'] = mediaSize as int?;
}
if (key != null) {
m['key'] = key;
if (key != notSpecified) {
m['key'] = key as String?;
}
if (iv != null) {
m['iv'] = iv;
if (iv != notSpecified) {
m['iv'] = iv as String?;
}
if (encryptionScheme != null) {
m['encryptionScheme'] = encryptionScheme;
if (encryptionScheme != notSpecified) {
m['encryptionScheme'] = encryptionScheme as String?;
}
if (isDownloading != null) {
m['isDownloading'] = boolToInt(isDownloading);
@ -439,11 +461,14 @@ class DatabaseService {
if (isUploading != null) {
m['isUploading'] = boolToInt(isUploading);
}
if (sid != null) {
m['sid'] = sid;
if (sid != notSpecified) {
m['sid'] = sid as String?;
}
if (originId != null) {
m['originId'] = originId;
if (originId != notSpecified) {
m['originId'] = originId as String?;
}
if (isRetracted != null) {
m['isRetracted'] = boolToInt(isRetracted);
}
await _db.update(

View File

@ -0,0 +1,11 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/shared/models/preference.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV3ToV4(Database db) async {
// Mark all messages as not retracted
await db.execute(
'ALTER TABLE $messsagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
);
}

View File

@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/shared/models/message.dart';
class MessageService {
@ -120,28 +121,31 @@ class MessageService {
/// Wrapper around [DatabaseService]'s updateMessage that updates the cache
Future<Message> updateMessage(int id, {
String? mediaUrl,
String? mediaType,
Object? body = notSpecified,
Object? mediaUrl = notSpecified,
Object? mediaType = notSpecified,
bool? received,
bool? displayed,
bool? acked,
int? errorType,
int? warningType,
Object? errorType = notSpecified,
Object? warningType = notSpecified,
bool? isFileUploadNotification,
String? srcUrl,
String? key,
String? iv,
String? encryptionScheme,
int? mediaWidth,
int? mediaHeight,
int? mediaSize,
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,
String? originId,
String? sid,
Object? originId = notSpecified,
Object? sid = notSpecified,
bool? isRetracted,
}) async {
final newMessage = await GetIt.I.get<DatabaseService>().updateMessage(
id,
body: body,
mediaUrl: mediaUrl,
mediaType: mediaType,
received: received,
@ -161,6 +165,7 @@ class MessageService {
isDownloading: isDownloading,
originId: originId,
sid: sid,
isRetracted: isRetracted,
);
if (_messageCache.containsKey(newMessage.conversationJid)) {

View File

@ -0,0 +1,4 @@
class _NotSpecifiedValue { const _NotSpecifiedValue(); }
/// A value used for indicating that a value is not specified.
const notSpecified = _NotSpecifiedValue();

View File

@ -198,6 +198,7 @@ Future<void> entrypoint() async {
EmeManager(),
CryptographicHashManager(),
DelayedDeliveryManager(),
MessageRetractionManager(),
])
..registerFeatureNegotiators([
ResourceBindingNegotiator(),

View File

@ -740,6 +740,37 @@ class XmppService {
&& implies(event.oob != null, event.body == event.oob?.url);
}
/// Handle a message retraction given the MessageEvent [event].
Future<void> _handleMessageRetraction(MessageEvent event, String conversationJid) async {
final msg = await GetIt.I.get<DatabaseService>().getMessageByOriginId(
event.messageRetraction!.id,
conversationJid,
);
if (msg == null) {
_log.finest('Got message retraction for origin Id ${event.messageRetraction!.id}, but did not find the message');
return;
}
// TODO(PapaTutuWawa): Change the lastMessageBody of the conversation if that message was retracted
final retractedMessage = await GetIt.I.get<MessageService>().updateMessage(
msg.id,
mediaUrl: null,
mediaType: null,
warningType: null,
errorType: null,
srcUrl: null,
key: null,
iv: null,
encryptionScheme: null,
mediaWidth: null,
mediaHeight: null,
mediaSize: null,
isRetracted: true,
);
sendEvent(MessageUpdatedEvent(message: retractedMessage));
}
/// Returns true if a file should be automatically downloaded. If it should not, it
/// returns false.
/// [conversationJid] refers to the JID of the conversation the message was received in.
@ -763,6 +794,11 @@ class XmppService {
await _handleFileUploadNotificationReplacement(event, conversationJid);
return;
}
if (event.messageRetraction != null) {
await _handleMessageRetraction(event, conversationJid);
return;
}
// Stop the processing here if the event does not describe a displayable message
if (!_isMessageEventMessage(event) && event.other['encryption_error'] == null) return;

View File

@ -53,6 +53,7 @@ class Message with _$Message {
@Default(false) bool received,
@Default(false) bool displayed,
@Default(false) bool acked,
@Default(false) bool isRetracted,
String? originId,
Message? quotes,
String? filename,
@ -80,6 +81,7 @@ class Message with _$Message {
'ciphertextHashes': _optionalJsonDecode(json['ciphertextHashes'] as String?),
'isDownloading': intToBool(json['isDownloading']! as int),
'isUploading': intToBool(json['isUploading']! as int),
'isRetracted': intToBool(json['isRetracted']! as int),
}).copyWith(quotes: quotes);
}
@ -102,6 +104,7 @@ class Message with _$Message {
'ciphertextHashes': _optionalJsonEncode(ciphertextHashes),
'isDownloading': boolToInt(isDownloading),
'isUploading': boolToInt(isUploading),
'isRetracted': boolToInt(isRetracted),
};
}

View File

@ -34,6 +34,18 @@ class TextChatWidget extends StatelessWidget {
final bool sent;
final Widget? topWidget;
String getMessageText() {
if (message.isError()) {
return errorTypeToText(message.errorType!);
}
if (message.isRetracted) {
return 'RETRACTED';
}
return message.body;
}
@override
Widget build(BuildContext context) {
final fontsize = EmojiUtil.hasOnlyEmojis(
@ -50,11 +62,9 @@ class TextChatWidget extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ParsedText(
text: message.isError() ?
errorTypeToText(message.errorType!) :
message.body,
text: getMessageText(),
style: TextStyle(
color: message.isError() ?
color: message.isError() || message.isRetracted ?
Colors.grey :
const Color(0xf9ebffff),
fontSize: fontsize,

View File

@ -750,9 +750,9 @@ packages:
moxxmpp:
dependency: "direct main"
description:
name: moxxmpp
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
source: hosted
path: "../moxxmpp/packages/moxxmpp"
relative: true
source: path
version: "0.1.3+1"
moxxmpp_socket_tcp:
dependency: "direct main"

View File

@ -127,6 +127,8 @@ dependency_overrides:
git:
url: https://codeberg.org/PapaTutuWawa/omemo_dart.git
rev: c68471349ab1b347ec9ad54651265710842c50b7
moxxmpp:
path: ../moxxmpp/packages/moxxmpp
extra_licenses:
- name: undraw.co