Merge pull request 'OMEMO Rework' (#286) from omemo-rework into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/286
This commit is contained in:
commit
8913977c0a
@ -170,7 +170,14 @@
|
|||||||
"stickerPickerNoStickersLine1": "You have no sticker packs installed.",
|
"stickerPickerNoStickersLine1": "You have no sticker packs installed.",
|
||||||
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
|
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
|
||||||
"stickerSettings": "Sticker settings",
|
"stickerSettings": "Sticker settings",
|
||||||
"newDeviceMessage": "${title} added a new encryption device",
|
"newDeviceMessage": {
|
||||||
|
"one": "A new device has been added",
|
||||||
|
"other": "Multiple new devices have been added"
|
||||||
|
},
|
||||||
|
"replacedDeviceMessage": {
|
||||||
|
"one": "A device has been changed",
|
||||||
|
"other": "Multiple devices have been added"
|
||||||
|
},
|
||||||
"messageHint": "Send a message...",
|
"messageHint": "Send a message...",
|
||||||
"sendImages": "Send images",
|
"sendImages": "Send images",
|
||||||
"sendFiles": "Send files",
|
"sendFiles": "Send files",
|
||||||
|
@ -170,7 +170,14 @@
|
|||||||
"stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.",
|
"stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.",
|
||||||
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
|
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
|
||||||
"stickerSettings": "Stickereinstellungen",
|
"stickerSettings": "Stickereinstellungen",
|
||||||
"newDeviceMessage": "${title} hat ein neues Verschlüsselungsgerät hinzugefügt",
|
"newDeviceMessage": {
|
||||||
|
"one": "Ein neues Gerät wurde hinzugefügt",
|
||||||
|
"other": "Mehrere neue Geräte wurden hinzugefügt"
|
||||||
|
},
|
||||||
|
"replacedDeviceMessage": {
|
||||||
|
"one": "Ein Gerät hat sich verändert",
|
||||||
|
"other": "Mehrere Geräte haben sich verändert"
|
||||||
|
},
|
||||||
"messageHint": "Nachricht senden...",
|
"messageHint": "Nachricht senden...",
|
||||||
"sendImages": "Bilder senden",
|
"sendImages": "Bilder senden",
|
||||||
"sendFiles": "Dateien senden",
|
"sendFiles": "Dateien senden",
|
||||||
|
@ -281,7 +281,8 @@ class ContactsService {
|
|||||||
return cs.updateConversation(
|
return cs.updateConversation(
|
||||||
contact.jid,
|
contact.jid,
|
||||||
contactId: contact.id,
|
contactId: contact.id,
|
||||||
contactAvatarPath: contact.thumbnail != null ? contactAvatarPath : null,
|
contactAvatarPath:
|
||||||
|
contact.thumbnail != null ? contactAvatarPath : null,
|
||||||
contactDisplayName: contact.displayName,
|
contactDisplayName: contact.displayName,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -3,13 +3,6 @@ const messagesTable = 'Messages';
|
|||||||
const rosterTable = 'RosterItems';
|
const rosterTable = 'RosterItems';
|
||||||
const mediaTable = 'SharedMedia';
|
const mediaTable = 'SharedMedia';
|
||||||
const preferenceTable = 'Preferences';
|
const preferenceTable = 'Preferences';
|
||||||
const omemoDeviceTable = 'OmemoDevices';
|
|
||||||
const omemoDeviceListTable = 'OmemoDeviceList';
|
|
||||||
const omemoRatchetsTable = 'OmemoSessions';
|
|
||||||
const omemoTrustCacheTable = 'OmemoTrustCacheList';
|
|
||||||
const omemoTrustDeviceListTable = 'OmemoTrustDeviceList';
|
|
||||||
const omemoTrustEnableListTable = 'OmemoTrustEnableList';
|
|
||||||
const omemoFingerprintCache = 'OmemoFingerprintCache';
|
|
||||||
const xmppStateTable = 'XmppState';
|
const xmppStateTable = 'XmppState';
|
||||||
const contactsTable = 'Contacts';
|
const contactsTable = 'Contacts';
|
||||||
const stickersTable = 'Stickers';
|
const stickersTable = 'Stickers';
|
||||||
@ -19,6 +12,10 @@ const subscriptionsTable = 'SubscriptionRequests';
|
|||||||
const fileMetadataTable = 'FileMetadata';
|
const fileMetadataTable = 'FileMetadata';
|
||||||
const fileMetadataHashesTable = 'FileMetadataHashes';
|
const fileMetadataHashesTable = 'FileMetadataHashes';
|
||||||
const reactionsTable = 'Reactions';
|
const reactionsTable = 'Reactions';
|
||||||
|
const omemoDevicesTable = 'OmemoDevices';
|
||||||
|
const omemoDeviceListTable = 'OmemoDeviceList';
|
||||||
|
const omemoRatchetsTable = 'OmemoRatchets';
|
||||||
|
const omemoTrustTable = 'OmemoTrustTable';
|
||||||
|
|
||||||
const typeString = 0;
|
const typeString = 0;
|
||||||
const typeInt = 1;
|
const typeInt = 1;
|
||||||
|
@ -18,7 +18,8 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
await db.execute('''
|
await db.execute(
|
||||||
|
'''
|
||||||
CREATE TABLE $messagesTable (
|
CREATE TABLE $messagesTable (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
sender TEXT NOT NULL,
|
sender TEXT NOT NULL,
|
||||||
@ -46,13 +47,15 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
pseudoMessageData TEXT,
|
pseudoMessageData TEXT,
|
||||||
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
|
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
|
||||||
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
|
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
|
||||||
)''');
|
)''',
|
||||||
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
|
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reactions
|
// Reactions
|
||||||
await db.execute('''
|
await db.execute(
|
||||||
|
'''
|
||||||
CREATE TABLE $reactionsTable (
|
CREATE TABLE $reactionsTable (
|
||||||
senderJid TEXT NOT NULL,
|
senderJid TEXT NOT NULL,
|
||||||
emoji TEXT NOT NULL,
|
emoji TEXT NOT NULL,
|
||||||
@ -60,13 +63,15 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
CONSTRAINT pk_sender PRIMARY KEY (senderJid, emoji, message_id),
|
CONSTRAINT pk_sender PRIMARY KEY (senderJid, emoji, message_id),
|
||||||
CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
|
CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)''');
|
)''',
|
||||||
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, senderJid)',
|
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, senderJid)',
|
||||||
);
|
);
|
||||||
|
|
||||||
// File metadata
|
// File metadata
|
||||||
await db.execute('''
|
await db.execute(
|
||||||
|
'''
|
||||||
CREATE TABLE $fileMetadataTable (
|
CREATE TABLE $fileMetadataTable (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
path TEXT,
|
path TEXT,
|
||||||
@ -83,8 +88,10 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
cipherTextHashes TEXT,
|
cipherTextHashes TEXT,
|
||||||
filename TEXT NOT NULL,
|
filename TEXT NOT NULL,
|
||||||
size INTEGER
|
size INTEGER
|
||||||
)''');
|
)''',
|
||||||
await db.execute('''
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
CREATE TABLE $fileMetadataHashesTable (
|
CREATE TABLE $fileMetadataHashesTable (
|
||||||
algorithm TEXT NOT NULL,
|
algorithm TEXT NOT NULL,
|
||||||
value TEXT NOT NULL,
|
value TEXT NOT NULL,
|
||||||
@ -92,7 +99,8 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
CONSTRAINT f_primarykey PRIMARY KEY (algorithm, value),
|
CONSTRAINT f_primarykey PRIMARY KEY (algorithm, value),
|
||||||
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
|
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)''');
|
)''',
|
||||||
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)',
|
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)',
|
||||||
);
|
);
|
||||||
@ -103,7 +111,8 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
CREATE TABLE $conversationsTable (
|
CREATE TABLE $conversationsTable (
|
||||||
jid TEXT NOT NULL PRIMARY KEY,
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
avatarUrl TEXT NOT NULL,
|
avatarPath TEXT NOT NULL,
|
||||||
|
avatarHash TEXT,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
lastChangeTimestamp INTEGER NOT NULL,
|
lastChangeTimestamp INTEGER NOT NULL,
|
||||||
unreadCounter INTEGER NOT NULL,
|
unreadCounter INTEGER NOT NULL,
|
||||||
@ -124,11 +133,13 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Contacts
|
// Contacts
|
||||||
await db.execute('''
|
await db.execute(
|
||||||
|
'''
|
||||||
CREATE TABLE $contactsTable (
|
CREATE TABLE $contactsTable (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
jid TEXT NOT NULL
|
jid TEXT NOT NULL
|
||||||
)''');
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
// Roster
|
// Roster
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@ -137,7 +148,7 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
jid TEXT NOT NULL,
|
jid TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
avatarUrl TEXT NOT NULL,
|
avatarPath TEXT NOT NULL,
|
||||||
avatarHash TEXT NOT NULL,
|
avatarHash TEXT NOT NULL,
|
||||||
subscription TEXT NOT NULL,
|
subscription TEXT NOT NULL,
|
||||||
ask TEXT NOT NULL,
|
ask TEXT NOT NULL,
|
||||||
@ -188,72 +199,58 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
// OMEMO
|
// OMEMO
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $omemoRatchetsTable (
|
CREATE TABLE $omemoDevicesTable (
|
||||||
id INTEGER NOT NULL,
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
jid TEXT NOT NULL,
|
id INTEGER NOT NULL,
|
||||||
dhs TEXT NOT NULL,
|
ikPub TEXT NOT NULL,
|
||||||
dhs_pub TEXT NOT NULL,
|
ik TEXT NOT NULL,
|
||||||
dhr TEXT,
|
spkPub TEXT NOT NULL,
|
||||||
rk TEXT NOT NULL,
|
spk TEXT NOT NULL,
|
||||||
cks TEXT,
|
spkId INTEGER NOT NULL,
|
||||||
ckr TEXT,
|
spkSig TEXT NOT NULL,
|
||||||
ns INTEGER NOT NULL,
|
oldSpkPub TEXT,
|
||||||
nr INTEGER NOT NULL,
|
oldSpk TEXT,
|
||||||
pn INTEGER NOT NULL,
|
oldSpkId INTEGER,
|
||||||
ik_pub TEXT NOT NULL,
|
opks TEXT NOT NULL
|
||||||
session_ad TEXT NOT NULL,
|
|
||||||
acknowledged INTEGER NOT NULL,
|
|
||||||
mkskipped TEXT NOT NULL,
|
|
||||||
kex_timestamp INTEGER NOT NULL,
|
|
||||||
kex TEXT,
|
|
||||||
PRIMARY KEY (jid, id)
|
|
||||||
)''',
|
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'''
|
|
||||||
CREATE TABLE $omemoTrustCacheTable (
|
|
||||||
key TEXT PRIMARY KEY NOT NULL,
|
|
||||||
trust INTEGER NOT NULL
|
|
||||||
)''',
|
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'''
|
|
||||||
CREATE TABLE $omemoTrustDeviceListTable (
|
|
||||||
jid TEXT NOT NULL,
|
|
||||||
device INTEGER NOT NULL
|
|
||||||
)''',
|
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'''
|
|
||||||
CREATE TABLE $omemoTrustEnableListTable (
|
|
||||||
key TEXT PRIMARY KEY NOT NULL,
|
|
||||||
enabled INTEGER NOT NULL
|
|
||||||
)''',
|
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'''
|
|
||||||
CREATE TABLE $omemoDeviceTable (
|
|
||||||
jid TEXT NOT NULL,
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
data TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (jid, id)
|
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $omemoDeviceListTable (
|
CREATE TABLE $omemoDeviceListTable (
|
||||||
jid TEXT NOT NULL,
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
id INTEGER NOT NULL,
|
devices TEXT NOT NULL
|
||||||
PRIMARY KEY (jid, id)
|
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $omemoFingerprintCache (
|
CREATE TABLE $omemoRatchetsTable (
|
||||||
jid TEXT NOT NULL,
|
jid TEXT NOT NULL,
|
||||||
id INTEGER NOT NULL,
|
device INTEGER NOT NULL,
|
||||||
fingerprint TEXT NOT NULL,
|
dhsPub TEXT NOT NULL,
|
||||||
PRIMARY KEY (jid, id)
|
dhs TEXT NOT NULL,
|
||||||
|
dhrPub TEXT,
|
||||||
|
rk TEXT NOT NULL,
|
||||||
|
cks TEXT,
|
||||||
|
ckr TEXT,
|
||||||
|
ns INTEGER NOT NULL,
|
||||||
|
nr INTEGER NOT NULL,
|
||||||
|
pn INTEGER NOT NULL,
|
||||||
|
ik TEXT NOT NULL,
|
||||||
|
ad TEXT NOT NULL,
|
||||||
|
skipped TEXT NOT NULL,
|
||||||
|
kex TEXT NOT NULL,
|
||||||
|
acked INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (jid, device)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoTrustTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
trust INTEGER NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (jid, device)
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -42,6 +42,8 @@ import 'package:moxxyv2/service/database/migrations/0002_reactions_2.dart';
|
|||||||
import 'package:moxxyv2/service/database/migrations/0002_shared_media.dart';
|
import 'package:moxxyv2/service/database/migrations/0002_shared_media.dart';
|
||||||
import 'package:moxxyv2/service/database/migrations/0002_sticker_metadata.dart';
|
import 'package:moxxyv2/service/database/migrations/0002_sticker_metadata.dart';
|
||||||
import 'package:moxxyv2/service/database/migrations/0003_avatar_hashes.dart';
|
import 'package:moxxyv2/service/database/migrations/0003_avatar_hashes.dart';
|
||||||
|
import 'package:moxxyv2/service/database/migrations/0003_new_omemo.dart';
|
||||||
|
import 'package:moxxyv2/service/database/migrations/0003_new_omemo_pseudo_messages.dart';
|
||||||
import 'package:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart';
|
import 'package:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:random_string/random_string.dart';
|
import 'package:random_string/random_string.dart';
|
||||||
@ -148,6 +150,8 @@ const List<DatabaseMigration<Database>> migrations = [
|
|||||||
DatabaseMigration(37, upgradeFromV36ToV37),
|
DatabaseMigration(37, upgradeFromV36ToV37),
|
||||||
DatabaseMigration(38, upgradeFromV37ToV38),
|
DatabaseMigration(38, upgradeFromV37ToV38),
|
||||||
DatabaseMigration(39, upgradeFromV38ToV39),
|
DatabaseMigration(39, upgradeFromV38ToV39),
|
||||||
|
DatabaseMigration(40, upgradeFromV39ToV40),
|
||||||
|
DatabaseMigration(41, upgradeFromV40ToV41),
|
||||||
];
|
];
|
||||||
|
|
||||||
class DatabaseService {
|
class DatabaseService {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import 'package:moxxyv2/service/database/constants.dart';
|
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
Future<void> upgradeFromV12ToV13(Database db) async {
|
Future<void> upgradeFromV12ToV13(Database db) async {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $omemoFingerprintCache (
|
CREATE TABLE OmemoFingerprintCache (
|
||||||
jid TEXT NOT NULL,
|
jid TEXT NOT NULL,
|
||||||
id INTEGER NOT NULL,
|
id INTEGER NOT NULL,
|
||||||
fingerprint TEXT NOT NULL,
|
fingerprint TEXT NOT NULL,
|
||||||
|
72
lib/service/database/migrations/0003_new_omemo.dart
Normal file
72
lib/service/database/migrations/0003_new_omemo.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV39ToV40(Database db) async {
|
||||||
|
// Remove the old tables
|
||||||
|
await db.execute('DROP TABLE OmemoDevices');
|
||||||
|
await db.execute('DROP TABLE OmemoDeviceList');
|
||||||
|
await db.execute('DROP TABLE OmemoTrustCacheList');
|
||||||
|
await db.execute('DROP TABLE OmemoTrustDeviceList');
|
||||||
|
await db.execute('DROP TABLE OmemoTrustEnableList');
|
||||||
|
await db.execute('DROP TABLE OmemoFingerprintCache');
|
||||||
|
|
||||||
|
// Create the new tables
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoDevicesTable (
|
||||||
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
ikPub TEXT NOT NULL,
|
||||||
|
ik TEXT NOT NULL,
|
||||||
|
spkPub TEXT NOT NULL,
|
||||||
|
spk TEXT NOT NULL,
|
||||||
|
spkId INTEGER NOT NULL,
|
||||||
|
spkSig TEXT NOT NULL,
|
||||||
|
oldSpkPub TEXT,
|
||||||
|
oldSpk TEXT,
|
||||||
|
oldSpkId INTEGER,
|
||||||
|
opks TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoDeviceListTable (
|
||||||
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
devices TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoRatchetsTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
dhsPub TEXT NOT NULL,
|
||||||
|
dhs TEXT NOT NULL,
|
||||||
|
dhrPub TEXT,
|
||||||
|
rk TEXT NOT NULL,
|
||||||
|
cks TEXT,
|
||||||
|
ckr TEXT,
|
||||||
|
ns INTEGER NOT NULL,
|
||||||
|
nr INTEGER NOT NULL,
|
||||||
|
pn INTEGER NOT NULL,
|
||||||
|
ik TEXT NOT NULL,
|
||||||
|
ad TEXT NOT NULL,
|
||||||
|
skipped TEXT NOT NULL,
|
||||||
|
kex TEXT NOT NULL,
|
||||||
|
acked INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (jid, device)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoTrustTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
trust INTEGER NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (jid, device)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV40ToV41(Database db) async {
|
||||||
|
final messages = await db.query(
|
||||||
|
messagesTable,
|
||||||
|
where: 'pseudoMessageType IS NOT NULL',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final message in messages) {
|
||||||
|
await db.insert(
|
||||||
|
messagesTable,
|
||||||
|
{
|
||||||
|
...message,
|
||||||
|
'pseudoMessageData': jsonEncode({
|
||||||
|
'ratchetsAdded': 1,
|
||||||
|
'ratchetsReplaced': 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -776,7 +776,7 @@ Future<void> performGetOmemoFingerprints(
|
|||||||
final omemo = GetIt.I.get<OmemoService>();
|
final omemo = GetIt.I.get<OmemoService>();
|
||||||
sendEvent(
|
sendEvent(
|
||||||
GetConversationOmemoFingerprintsResult(
|
GetConversationOmemoFingerprintsResult(
|
||||||
fingerprints: await omemo.getOmemoKeysForJid(command.jid),
|
fingerprints: await omemo.getFingerprintsForJid(command.jid),
|
||||||
),
|
),
|
||||||
id: id,
|
id: id,
|
||||||
);
|
);
|
||||||
@ -789,7 +789,7 @@ Future<void> performEnableOmemoKey(
|
|||||||
final id = extra as String;
|
final id = extra as String;
|
||||||
|
|
||||||
final omemo = GetIt.I.get<OmemoService>();
|
final omemo = GetIt.I.get<OmemoService>();
|
||||||
await omemo.setOmemoKeyEnabled(
|
await omemo.setDeviceEnablement(
|
||||||
command.jid,
|
command.jid,
|
||||||
command.deviceId,
|
command.deviceId,
|
||||||
command.enabled,
|
command.enabled,
|
||||||
@ -805,10 +805,14 @@ Future<void> performRecreateSessions(
|
|||||||
RecreateSessionsCommand command, {
|
RecreateSessionsCommand command, {
|
||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
await GetIt.I.get<OmemoService>().removeAllSessions(command.jid);
|
// Remove all ratchets
|
||||||
|
await GetIt.I.get<OmemoService>().removeAllRatchets(command.jid);
|
||||||
|
|
||||||
final conn = GetIt.I.get<XmppConnection>();
|
// And force the creation of new ones
|
||||||
await conn.getManagerById<BaseOmemoManager>(omemoManager)!.sendOmemoHeartbeat(
|
await GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<OmemoManager>(omemoManager)!
|
||||||
|
.sendOmemoHeartbeat(
|
||||||
command.jid,
|
command.jid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -837,14 +841,14 @@ Future<void> performGetOwnOmemoFingerprints(
|
|||||||
final id = extra as String;
|
final id = extra as String;
|
||||||
final os = GetIt.I.get<OmemoService>();
|
final os = GetIt.I.get<OmemoService>();
|
||||||
final xs = GetIt.I.get<XmppService>();
|
final xs = GetIt.I.get<XmppService>();
|
||||||
await os.ensureInitialized();
|
|
||||||
|
|
||||||
final jid = (await xs.getConnectionSettings())!.jid;
|
final jid = (await xs.getConnectionSettings())!.jid;
|
||||||
|
final device = await os.getDevice();
|
||||||
sendEvent(
|
sendEvent(
|
||||||
GetOwnOmemoFingerprintsResult(
|
GetOwnOmemoFingerprintsResult(
|
||||||
ownDeviceFingerprint: await os.getDeviceFingerprint(),
|
ownDeviceFingerprint: await device.getFingerprint(),
|
||||||
ownDeviceId: await os.getDeviceId(),
|
ownDeviceId: device.id,
|
||||||
fingerprints: await os.getOwnFingerprints(jid),
|
fingerprints: await os.getFingerprintsForJid(jid.toString()),
|
||||||
),
|
),
|
||||||
id: id,
|
id: id,
|
||||||
);
|
);
|
||||||
@ -856,7 +860,7 @@ Future<void> performRemoveOwnDevice(
|
|||||||
}) async {
|
}) async {
|
||||||
await GetIt.I
|
await GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getManagerById<BaseOmemoManager>(omemoManager)!
|
.getManagerById<OmemoManager>(omemoManager)!
|
||||||
.deleteDevice(command.deviceId);
|
.deleteDevice(command.deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,9 +869,7 @@ Future<void> performRegenerateOwnDevice(
|
|||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
final id = extra as String;
|
final id = extra as String;
|
||||||
final jid =
|
final device = await GetIt.I.get<OmemoService>().regenerateDevice();
|
||||||
GetIt.I.get<XmppConnection>().connectionSettings.jid.toBare().toString();
|
|
||||||
final device = await GetIt.I.get<OmemoService>().regenerateDevice(jid);
|
|
||||||
|
|
||||||
sendEvent(
|
sendEvent(
|
||||||
RegenerateOwnDeviceResult(device: device),
|
RegenerateOwnDeviceResult(device: device),
|
||||||
@ -1041,9 +1043,9 @@ Future<void> performMarkDeviceVerified(
|
|||||||
MarkOmemoDeviceAsVerifiedCommand command, {
|
MarkOmemoDeviceAsVerifiedCommand command, {
|
||||||
dynamic extra,
|
dynamic extra,
|
||||||
}) async {
|
}) async {
|
||||||
await GetIt.I.get<OmemoService>().verifyDevice(
|
await GetIt.I.get<OmemoService>().setDeviceVerified(
|
||||||
command.deviceId,
|
|
||||||
command.jid,
|
command.jid,
|
||||||
|
command.deviceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ FROM (SELECT * FROM $messagesTable WHERE $query ORDER BY timestamp DESC LIMIT $s
|
|||||||
bool isDownloading = false,
|
bool isDownloading = false,
|
||||||
bool isUploading = false,
|
bool isUploading = false,
|
||||||
String? stickerPackId,
|
String? stickerPackId,
|
||||||
int? pseudoMessageType,
|
PseudoMessageType? pseudoMessageType,
|
||||||
Map<String, dynamic>? pseudoMessageData,
|
Map<String, dynamic>? pseudoMessageData,
|
||||||
bool received = false,
|
bool received = false,
|
||||||
bool displayed = false,
|
bool displayed = false,
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
|
||||||
import 'package:moxxyv2/service/conversation.dart';
|
|
||||||
import 'package:moxxyv2/service/omemo/omemo.dart';
|
|
||||||
import 'package:omemo_dart/omemo_dart.dart';
|
|
||||||
|
|
||||||
class MoxxyOmemoManager extends BaseOmemoManager {
|
|
||||||
MoxxyOmemoManager() : super();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<OmemoManager> getOmemoManager() async {
|
|
||||||
final os = GetIt.I.get<OmemoService>();
|
|
||||||
await os.ensureInitialized();
|
|
||||||
return os.omemoManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza) async {
|
|
||||||
// Never encrypt stanzas that contain PubSub elements
|
|
||||||
if (stanza.firstTag('pubsub', xmlns: pubsubXmlns) != null ||
|
|
||||||
stanza.firstTag('pubsub', xmlns: pubsubOwnerXmlns) != null ||
|
|
||||||
stanza.firstTagByXmlns(carbonsXmlns) != null ||
|
|
||||||
stanza.firstTagByXmlns(rosterXmlns) != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt when the conversation is set to use OMEMO.
|
|
||||||
return GetIt.I
|
|
||||||
.get<ConversationService>()
|
|
||||||
.shouldEncryptForConversation(toJid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MoxxyBTBVTrustManager extends BlindTrustBeforeVerificationTrustManager {
|
|
||||||
MoxxyBTBVTrustManager(
|
|
||||||
Map<RatchetMapKey, BTBVTrustState> trustCache,
|
|
||||||
Map<RatchetMapKey, bool> enablementCache,
|
|
||||||
Map<String, List<int>> devices,
|
|
||||||
) : super(
|
|
||||||
trustCache: trustCache,
|
|
||||||
enablementCache: enablementCache,
|
|
||||||
devices: devices,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> commitState() async {
|
|
||||||
await GetIt.I.get<OmemoService>().commitTrustManager(await toJson());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,213 +1,43 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:hex/hex.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart' as moxxmpp;
|
import 'package:moxxmpp/moxxmpp.dart' as moxxmpp;
|
||||||
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/message.dart';
|
import 'package:moxxyv2/service/message.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/omemo.dart';
|
|
||||||
import 'package:moxxyv2/service/omemo/implementations.dart';
|
import 'package:moxxyv2/service/omemo/implementations.dart';
|
||||||
import 'package:moxxyv2/service/omemo/types.dart';
|
import 'package:moxxyv2/service/omemo/persistence.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/service/xmpp_state.dart';
|
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
import 'package:moxxyv2/shared/models/message.dart';
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
import 'package:moxxyv2/shared/models/omemo_device.dart' as model;
|
import 'package:moxxyv2/shared/models/omemo_device.dart' as model;
|
||||||
import 'package:omemo_dart/omemo_dart.dart';
|
import 'package:omemo_dart/omemo_dart.dart';
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
class OmemoDoubleRatchetWrapper {
|
|
||||||
OmemoDoubleRatchetWrapper(this.ratchet, this.id, this.jid);
|
|
||||||
final OmemoDoubleRatchet ratchet;
|
|
||||||
final int id;
|
|
||||||
final String jid;
|
|
||||||
}
|
|
||||||
|
|
||||||
class OmemoService {
|
class OmemoService {
|
||||||
|
/// Logger.
|
||||||
final Logger _log = Logger('OmemoService');
|
final Logger _log = Logger('OmemoService');
|
||||||
|
|
||||||
|
/// Flag indicating whether we are initialized.
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
|
||||||
|
/// Flag indicating whether the initialization is currently running.
|
||||||
|
bool _running = false;
|
||||||
|
|
||||||
|
/// Lock guarding access to [_waitingForInitialization], [_running], and [_initialized].
|
||||||
final Lock _lock = Lock();
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
/// Queue for code that is waiting on the service initialization.
|
||||||
final Queue<Completer<void>> _waitingForInitialization =
|
final Queue<Completer<void>> _waitingForInitialization =
|
||||||
Queue<Completer<void>>();
|
Queue<Completer<void>>();
|
||||||
final Map<String, Map<int, String>> _fingerprintCache = {};
|
|
||||||
|
|
||||||
late OmemoManager omemoManager;
|
/// The manager to use for OMEMO.
|
||||||
|
late OmemoManager _omemoManager;
|
||||||
|
|
||||||
Future<void> initializeIfNeeded(String jid) async {
|
/// Access the underlying [OmemoManager].
|
||||||
final done = await _lock.synchronized(() => _initialized);
|
Future<OmemoManager> getOmemoManager() async {
|
||||||
if (done) return;
|
await ensureInitialized();
|
||||||
|
return _omemoManager;
|
||||||
final device = await _loadOmemoDevice(jid);
|
|
||||||
final ratchetMap = <RatchetMapKey, OmemoDoubleRatchet>{};
|
|
||||||
final deviceList = <String, List<int>>{};
|
|
||||||
if (device == null) {
|
|
||||||
_log.info('No OMEMO marker found. Generating OMEMO identity...');
|
|
||||||
} else {
|
|
||||||
_log.info('OMEMO marker found. Restoring OMEMO state...');
|
|
||||||
for (final ratchet in await _loadRatchets()) {
|
|
||||||
final key = RatchetMapKey(ratchet.jid, ratchet.id);
|
|
||||||
ratchetMap[key] = ratchet.ratchet;
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceList.addAll(await _loadOmemoDeviceList());
|
|
||||||
}
|
|
||||||
|
|
||||||
final om = GetIt.I
|
|
||||||
.get<moxxmpp.XmppConnection>()
|
|
||||||
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!;
|
|
||||||
omemoManager = OmemoManager(
|
|
||||||
device ?? await compute(generateNewIdentityImpl, jid),
|
|
||||||
await loadTrustManager(),
|
|
||||||
om.sendEmptyMessageImpl,
|
|
||||||
om.fetchDeviceList,
|
|
||||||
om.fetchDeviceBundle,
|
|
||||||
om.subscribeToDeviceListImpl,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (device == null) {
|
|
||||||
await commitDevice(await omemoManager.getDevice());
|
|
||||||
await commitDeviceMap(<String, List<int>>{});
|
|
||||||
await commitTrustManager(await omemoManager.trustManager.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
omemoManager.initialize(
|
|
||||||
ratchetMap,
|
|
||||||
deviceList,
|
|
||||||
);
|
|
||||||
|
|
||||||
omemoManager.eventStream.listen((event) async {
|
|
||||||
if (event is RatchetModifiedEvent) {
|
|
||||||
await _saveRatchet(
|
|
||||||
OmemoDoubleRatchetWrapper(
|
|
||||||
event.ratchet,
|
|
||||||
event.deviceId,
|
|
||||||
event.jid,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (event.added) {
|
|
||||||
// Cache the fingerprint
|
|
||||||
final fingerprint = await event.ratchet.getOmemoFingerprint();
|
|
||||||
await _addFingerprintsToCache([
|
|
||||||
OmemoCacheTriple(
|
|
||||||
event.jid,
|
|
||||||
event.deviceId,
|
|
||||||
fingerprint,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (_fingerprintCache.containsKey(event.jid)) {
|
|
||||||
_fingerprintCache[event.jid]![event.deviceId] = fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
await addNewDeviceMessage(event.jid, event.deviceId);
|
|
||||||
}
|
|
||||||
} else if (event is DeviceListModifiedEvent) {
|
|
||||||
await commitDeviceMap(event.list);
|
|
||||||
} else if (event is DeviceModifiedEvent) {
|
|
||||||
await commitDevice(event.device);
|
|
||||||
|
|
||||||
// Publish it
|
|
||||||
await GetIt.I
|
|
||||||
.get<moxxmpp.XmppConnection>()
|
|
||||||
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!
|
|
||||||
.publishBundle(await event.device.toBundle());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await _lock.synchronized(() {
|
|
||||||
_initialized = true;
|
|
||||||
|
|
||||||
for (final c in _waitingForInitialization) {
|
|
||||||
c.complete();
|
|
||||||
}
|
|
||||||
_waitingForInitialization.clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a pseudo message saying that [jid] added a new device with id [deviceId].
|
|
||||||
/// If, however, [jid] is our own JID, then nothing is done.
|
|
||||||
Future<void> addNewDeviceMessage(String jid, int deviceId) async {
|
|
||||||
// Add a pseudo message if it is not about our own devices
|
|
||||||
final xmppState = await GetIt.I.get<XmppStateService>().getXmppState();
|
|
||||||
if (jid == xmppState.jid) return;
|
|
||||||
|
|
||||||
final ms = GetIt.I.get<MessageService>();
|
|
||||||
final message = await ms.addMessageFromData(
|
|
||||||
'',
|
|
||||||
DateTime.now().millisecondsSinceEpoch,
|
|
||||||
'',
|
|
||||||
jid,
|
|
||||||
'',
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
pseudoMessageType: pseudoMessageTypeNewDevice,
|
|
||||||
pseudoMessageData: <String, dynamic>{
|
|
||||||
'deviceId': deviceId,
|
|
||||||
'jid': jid,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
sendEvent(
|
|
||||||
MessageAddedEvent(
|
|
||||||
message: message,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<model.OmemoDevice> regenerateDevice(String jid) async {
|
|
||||||
// Prevent access to the session manager as it is (mostly) guarded ensureInitialized
|
|
||||||
await _lock.synchronized(() {
|
|
||||||
_initialized = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
_log.info('No OMEMO marker found. Generating OMEMO identity...');
|
|
||||||
final oldId = await omemoManager.getDeviceId();
|
|
||||||
|
|
||||||
// Clear the database
|
|
||||||
await _emptyOmemoSessionTables();
|
|
||||||
|
|
||||||
// Regenerate the identity in the background
|
|
||||||
final device = await compute(generateNewIdentityImpl, jid);
|
|
||||||
await omemoManager.replaceDevice(device);
|
|
||||||
await commitDevice(device);
|
|
||||||
await commitDeviceMap(<String, List<int>>{});
|
|
||||||
await commitTrustManager(await omemoManager.trustManager.toJson());
|
|
||||||
|
|
||||||
// Remove the old device
|
|
||||||
final omemo = GetIt.I
|
|
||||||
.get<moxxmpp.XmppConnection>()
|
|
||||||
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!;
|
|
||||||
await omemo.deleteDevice(oldId);
|
|
||||||
|
|
||||||
// Publish the new one
|
|
||||||
await omemo.publishBundle(await omemoManager.getDeviceBundle());
|
|
||||||
|
|
||||||
// Allow access again
|
|
||||||
await _lock.synchronized(() {
|
|
||||||
_initialized = true;
|
|
||||||
|
|
||||||
for (final c in _waitingForInitialization) {
|
|
||||||
c.complete();
|
|
||||||
}
|
|
||||||
_waitingForInitialization.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the OmemoDevice
|
|
||||||
return model.OmemoDevice(
|
|
||||||
await getDeviceFingerprint(),
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
await getDeviceId(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures that the code following this *AWAITED* call can access every method
|
/// Ensures that the code following this *AWAITED* call can access every method
|
||||||
@ -228,27 +58,79 @@ class OmemoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> commitDeviceMap(Map<String, List<int>> deviceMap) async {
|
/// Creates or loads the [OmemoManager] for the JID [jid].
|
||||||
await _saveOmemoDeviceList(deviceMap);
|
Future<void> initializeIfNeeded(String jid) async {
|
||||||
|
final done = await _lock.synchronized(() {
|
||||||
|
// Do nothing if we're already initialized
|
||||||
|
if (_initialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the execution if we're not yet running.
|
||||||
|
if (_running) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_running = true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (done) return;
|
||||||
|
|
||||||
|
final device = await loadOmemoDevice(jid);
|
||||||
|
if (device == null) {
|
||||||
|
_log.info('No OMEMO marker found. Generating OMEMO identity...');
|
||||||
|
} else {
|
||||||
|
_log.info('OMEMO marker found. Restoring OMEMO state...');
|
||||||
|
}
|
||||||
|
|
||||||
|
final om = GetIt.I
|
||||||
|
.get<moxxmpp.XmppConnection>()
|
||||||
|
.getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!;
|
||||||
|
|
||||||
|
_omemoManager = OmemoManager(
|
||||||
|
device ?? await compute(generateNewIdentityImpl, jid),
|
||||||
|
BlindTrustBeforeVerificationTrustManager(
|
||||||
|
commit: commitTrust,
|
||||||
|
loadData: loadTrust,
|
||||||
|
removeTrust: removeTrust,
|
||||||
|
),
|
||||||
|
om.sendEmptyMessageImpl,
|
||||||
|
om.fetchDeviceList,
|
||||||
|
om.fetchDeviceBundle,
|
||||||
|
om.subscribeToDeviceListImpl,
|
||||||
|
om.publishDeviceImpl,
|
||||||
|
commitDevice: commitDevice,
|
||||||
|
commitRatchets: commitRatchets,
|
||||||
|
commitDeviceList: commitDeviceList,
|
||||||
|
removeRatchets: removeRatchets,
|
||||||
|
loadRatchets: loadRatchets,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (device == null) {
|
||||||
|
await commitDevice(await _omemoManager.getDevice());
|
||||||
|
}
|
||||||
|
|
||||||
|
await _lock.synchronized(() {
|
||||||
|
_running = false;
|
||||||
|
_initialized = true;
|
||||||
|
|
||||||
|
for (final c in _waitingForInitialization) {
|
||||||
|
c.complete();
|
||||||
|
}
|
||||||
|
_waitingForInitialization.clear();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> commitDevice(OmemoDevice device) async {
|
Future<moxxmpp.OmemoError?> publishDeviceIfNeeded() async {
|
||||||
await _saveOmemoDevice(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Requests our device list and checks if the current device is in it. If not, then
|
|
||||||
/// it will be published.
|
|
||||||
Future<Object?> publishDeviceIfNeeded() async {
|
|
||||||
_log.finest('publishDeviceIfNeeded: Waiting for initialization...');
|
_log.finest('publishDeviceIfNeeded: Waiting for initialization...');
|
||||||
await ensureInitialized();
|
await ensureInitialized();
|
||||||
_log.finest('publishDeviceIfNeeded: Done');
|
_log.finest('publishDeviceIfNeeded: Done');
|
||||||
|
|
||||||
final conn = GetIt.I.get<moxxmpp.XmppConnection>();
|
final conn = GetIt.I.get<moxxmpp.XmppConnection>();
|
||||||
final omemo =
|
final omemo =
|
||||||
conn.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!;
|
conn.getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!;
|
||||||
final dm = conn.getManagerById<moxxmpp.DiscoManager>(moxxmpp.discoManager)!;
|
final dm = conn.getManagerById<moxxmpp.DiscoManager>(moxxmpp.discoManager)!;
|
||||||
final bareJid = conn.connectionSettings.jid.toBare();
|
final bareJid = conn.connectionSettings.jid.toBare();
|
||||||
final device = await omemoManager.getDevice();
|
final device = await _omemoManager.getDevice();
|
||||||
|
|
||||||
final bundlesRaw = await dm.discoItemsQuery(
|
final bundlesRaw = await dm.discoItemsQuery(
|
||||||
bareJid,
|
bareJid,
|
||||||
@ -256,7 +138,7 @@ class OmemoService {
|
|||||||
);
|
);
|
||||||
if (bundlesRaw.isType<moxxmpp.DiscoError>()) {
|
if (bundlesRaw.isType<moxxmpp.DiscoError>()) {
|
||||||
await omemo.publishBundle(await device.toBundle());
|
await omemo.publishBundle(await device.toBundle());
|
||||||
return bundlesRaw.get<moxxmpp.DiscoError>();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final bundleIds = bundlesRaw
|
final bundleIds = bundlesRaw
|
||||||
@ -285,469 +167,114 @@ class OmemoService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchFingerprintsAndCache(moxxmpp.JID jid) async {
|
Future<void> onNewConnection() async {
|
||||||
final bareJid = jid.toBare().toString();
|
|
||||||
final allDevicesRaw = await GetIt.I
|
|
||||||
.get<moxxmpp.XmppConnection>()
|
|
||||||
.getManagerById<moxxmpp.BaseOmemoManager>(moxxmpp.omemoManager)!
|
|
||||||
.retrieveDeviceBundles(jid);
|
|
||||||
if (allDevicesRaw.isType<List<OmemoBundle>>()) {
|
|
||||||
final allDevices = allDevicesRaw.get<List<OmemoBundle>>();
|
|
||||||
final map = <int, String>{};
|
|
||||||
final items = List<OmemoCacheTriple>.empty(growable: true);
|
|
||||||
for (final device in allDevices) {
|
|
||||||
final curveIk = await device.ik.toCurve25519();
|
|
||||||
final fingerprint = HEX.encode(await curveIk.getBytes());
|
|
||||||
map[device.id] = fingerprint;
|
|
||||||
items.add(OmemoCacheTriple(bareJid, device.id, fingerprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache them in memory
|
|
||||||
_fingerprintCache[bareJid] = map;
|
|
||||||
|
|
||||||
// Cache them in the database
|
|
||||||
await _addFingerprintsToCache(items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadOrFetchFingerprints(moxxmpp.JID jid) async {
|
|
||||||
final bareJid = jid.toBare().toString();
|
|
||||||
if (!_fingerprintCache.containsKey(bareJid)) {
|
|
||||||
// First try to load it from the database
|
|
||||||
final triples = await _getFingerprintsFromCache(bareJid);
|
|
||||||
if (triples.isEmpty) {
|
|
||||||
// We found no fingerprints in the database, so try to fetch them
|
|
||||||
await _fetchFingerprintsAndCache(jid);
|
|
||||||
} else {
|
|
||||||
// We have fetched fingerprints from the database
|
|
||||||
_fingerprintCache[bareJid] = Map<int, String>.fromEntries(
|
|
||||||
triples.map((triple) {
|
|
||||||
return MapEntry<int, String>(
|
|
||||||
triple.deviceId,
|
|
||||||
triple.fingerprint,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<model.OmemoDevice>> getOmemoKeysForJid(String jid) async {
|
|
||||||
await ensureInitialized();
|
await ensureInitialized();
|
||||||
|
await _omemoManager.onNewConnection();
|
||||||
// Get finger prints if we have to
|
|
||||||
await _loadOrFetchFingerprints(moxxmpp.JID.fromString(jid));
|
|
||||||
|
|
||||||
final keys = List<model.OmemoDevice>.empty(growable: true);
|
|
||||||
final tm =
|
|
||||||
omemoManager.trustManager as BlindTrustBeforeVerificationTrustManager;
|
|
||||||
final trustMap = await tm.getDevicesTrust(jid);
|
|
||||||
|
|
||||||
if (!_fingerprintCache.containsKey(jid)) return [];
|
|
||||||
for (final deviceId in _fingerprintCache[jid]!.keys) {
|
|
||||||
keys.add(
|
|
||||||
model.OmemoDevice(
|
|
||||||
_fingerprintCache[jid]![deviceId]!,
|
|
||||||
await tm.isTrusted(jid, deviceId),
|
|
||||||
trustMap[deviceId] == BTBVTrustState.verified,
|
|
||||||
await tm.isEnabled(jid, deviceId),
|
|
||||||
deviceId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> commitTrustManager(Map<String, dynamic> json) async {
|
Future<List<model.OmemoDevice>> getFingerprintsForJid(String jid) async {
|
||||||
await _saveTrustCache(
|
|
||||||
json['trust']! as Map<String, int>,
|
|
||||||
);
|
|
||||||
await _saveTrustEnablementList(
|
|
||||||
json['enable']! as Map<String, bool>,
|
|
||||||
);
|
|
||||||
await _saveTrustDeviceList(
|
|
||||||
json['devices']! as Map<String, List<int>>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<MoxxyBTBVTrustManager> loadTrustManager() async {
|
|
||||||
return MoxxyBTBVTrustManager(
|
|
||||||
await _loadTrustCache(),
|
|
||||||
await _loadTrustEnablementList(),
|
|
||||||
await _loadTrustDeviceList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setOmemoKeyEnabled(
|
|
||||||
String jid,
|
|
||||||
int deviceId,
|
|
||||||
bool enabled,
|
|
||||||
) async {
|
|
||||||
await ensureInitialized();
|
await ensureInitialized();
|
||||||
await omemoManager.trustManager.setEnabled(jid, deviceId, enabled);
|
final fingerprints = await _omemoManager.getFingerprintsForJid(jid) ?? [];
|
||||||
}
|
var trust = <int, BTBVTrustData>{};
|
||||||
|
|
||||||
Future<void> removeAllSessions(String jid) async {
|
await _omemoManager.withTrustManager(
|
||||||
await ensureInitialized();
|
|
||||||
await omemoManager.removeAllRatchets(jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> getDeviceId() async {
|
|
||||||
await ensureInitialized();
|
|
||||||
return omemoManager.getDeviceId();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> getDeviceFingerprint() => omemoManager.getDeviceFingerprint();
|
|
||||||
|
|
||||||
/// Returns a list of OmemoDevices for devices we have sessions with and other devices
|
|
||||||
/// published on [ownJid]'s devices PubSub node.
|
|
||||||
/// Note that the list is made so that the current device is excluded.
|
|
||||||
Future<List<model.OmemoDevice>> getOwnFingerprints(moxxmpp.JID ownJid) async {
|
|
||||||
final ownId = await getDeviceId();
|
|
||||||
final keys = List<model.OmemoDevice>.from(
|
|
||||||
await getOmemoKeysForJid(ownJid.toString()),
|
|
||||||
);
|
|
||||||
final bareJid = ownJid.toBare().toString();
|
|
||||||
|
|
||||||
// Get fingerprints if we have to
|
|
||||||
await _loadOrFetchFingerprints(ownJid);
|
|
||||||
|
|
||||||
final tm =
|
|
||||||
omemoManager.trustManager as BlindTrustBeforeVerificationTrustManager;
|
|
||||||
final trustMap = await tm.getDevicesTrust(bareJid);
|
|
||||||
|
|
||||||
for (final deviceId in _fingerprintCache[bareJid]!.keys) {
|
|
||||||
if (deviceId == ownId) continue;
|
|
||||||
if (keys.indexWhere((key) => key.deviceId == deviceId) != -1) continue;
|
|
||||||
|
|
||||||
final fingerprint = _fingerprintCache[bareJid]![deviceId]!;
|
|
||||||
keys.add(
|
|
||||||
model.OmemoDevice(
|
|
||||||
fingerprint,
|
|
||||||
await tm.isTrusted(bareJid, deviceId),
|
|
||||||
trustMap[deviceId] == BTBVTrustState.verified,
|
|
||||||
await tm.isEnabled(bareJid, deviceId),
|
|
||||||
deviceId,
|
|
||||||
hasSessionWith: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> verifyDevice(int deviceId, String jid) async {
|
|
||||||
final tm =
|
|
||||||
omemoManager.trustManager as BlindTrustBeforeVerificationTrustManager;
|
|
||||||
await tm.setDeviceTrust(
|
|
||||||
jid,
|
jid,
|
||||||
deviceId,
|
(tm) async {
|
||||||
BTBVTrustState.verified,
|
trust = await (tm as BlindTrustBeforeVerificationTrustManager)
|
||||||
|
.getDevicesTrust(jid);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// Tells omemo_dart, that certain caches are to be seen as invalidated.
|
return fingerprints.map((fp) {
|
||||||
void onNewConnection() {
|
return model.OmemoDevice(
|
||||||
if (_initialized) {
|
fp.fingerprint,
|
||||||
omemoManager.onNewConnection();
|
trust[fp.deviceId]?.trusted ?? false,
|
||||||
}
|
trust[fp.deviceId]?.state == BTBVTrustState.verified,
|
||||||
}
|
trust[fp.deviceId]?.enabled ?? false,
|
||||||
|
fp.deviceId,
|
||||||
/// Database methods
|
|
||||||
|
|
||||||
Future<List<OmemoDoubleRatchetWrapper>> _loadRatchets() async {
|
|
||||||
final results =
|
|
||||||
await GetIt.I.get<DatabaseService>().database.query(omemoRatchetsTable);
|
|
||||||
|
|
||||||
return results.map((ratchet) {
|
|
||||||
final json = jsonDecode(ratchet['mkskipped']! as String) as List<dynamic>;
|
|
||||||
final mkskipped = List<Map<String, dynamic>>.empty(growable: true);
|
|
||||||
for (final i in json) {
|
|
||||||
final element = i as Map<String, dynamic>;
|
|
||||||
mkskipped.add({
|
|
||||||
'key': element['key']! as String,
|
|
||||||
'public': element['public']! as String,
|
|
||||||
'n': element['n']! as int,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return OmemoDoubleRatchetWrapper(
|
|
||||||
OmemoDoubleRatchet.fromJson(
|
|
||||||
{
|
|
||||||
...ratchet,
|
|
||||||
'acknowledged': intToBool(ratchet['acknowledged']! as int),
|
|
||||||
'mkskipped': mkskipped,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ratchet['id']! as int,
|
|
||||||
ratchet['jid']! as String,
|
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveRatchet(OmemoDoubleRatchetWrapper ratchet) async {
|
Future<void> setDeviceEnablement(String jid, int device, bool state) async {
|
||||||
final json = await ratchet.ratchet.toJson();
|
await ensureInitialized();
|
||||||
await GetIt.I.get<DatabaseService>().database.insert(
|
await _omemoManager.withTrustManager(jid, (tm) async {
|
||||||
omemoRatchetsTable,
|
await (tm as BlindTrustBeforeVerificationTrustManager)
|
||||||
{
|
.setEnabled(jid, device, state);
|
||||||
...json,
|
|
||||||
'mkskipped': jsonEncode(json['mkskipped']),
|
|
||||||
'acknowledged': boolToInt(json['acknowledged']! as bool),
|
|
||||||
'jid': ratchet.jid,
|
|
||||||
'id': ratchet.id,
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<RatchetMapKey, BTBVTrustState>> _loadTrustCache() async {
|
|
||||||
final entries = await GetIt.I
|
|
||||||
.get<DatabaseService>()
|
|
||||||
.database
|
|
||||||
.query(omemoTrustCacheTable);
|
|
||||||
|
|
||||||
final mapEntries =
|
|
||||||
entries.map<MapEntry<RatchetMapKey, BTBVTrustState>>((entry) {
|
|
||||||
// TODO(PapaTutuWawa): Expose this from omemo_dart
|
|
||||||
BTBVTrustState state;
|
|
||||||
final value = entry['trust']! as int;
|
|
||||||
if (value == 1) {
|
|
||||||
state = BTBVTrustState.notTrusted;
|
|
||||||
} else if (value == 2) {
|
|
||||||
state = BTBVTrustState.blindTrust;
|
|
||||||
} else if (value == 3) {
|
|
||||||
state = BTBVTrustState.verified;
|
|
||||||
} else {
|
|
||||||
state = BTBVTrustState.notTrusted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MapEntry(
|
|
||||||
RatchetMapKey.fromJsonKey(entry['key']! as String),
|
|
||||||
state,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Map.fromEntries(mapEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveTrustCache(Map<String, int> cache) async {
|
Future<void> setDeviceVerified(String jid, int device) async {
|
||||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
await ensureInitialized();
|
||||||
|
await _omemoManager.withTrustManager(jid, (tm) async {
|
||||||
// ignore: cascade_invocations
|
await (tm as BlindTrustBeforeVerificationTrustManager)
|
||||||
batch.delete(omemoTrustCacheTable);
|
.setDeviceTrust(jid, device, BTBVTrustState.verified);
|
||||||
for (final entry in cache.entries) {
|
|
||||||
batch.insert(
|
|
||||||
omemoTrustCacheTable,
|
|
||||||
{
|
|
||||||
'key': entry.key,
|
|
||||||
'trust': entry.value,
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await batch.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<RatchetMapKey, bool>> _loadTrustEnablementList() async {
|
|
||||||
final entries = await GetIt.I
|
|
||||||
.get<DatabaseService>()
|
|
||||||
.database
|
|
||||||
.query(omemoTrustEnableListTable);
|
|
||||||
|
|
||||||
final mapEntries = entries.map<MapEntry<RatchetMapKey, bool>>((entry) {
|
|
||||||
return MapEntry(
|
|
||||||
RatchetMapKey.fromJsonKey(entry['key']! as String),
|
|
||||||
intToBool(entry['enabled']! as int),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Map.fromEntries(mapEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveTrustEnablementList(Map<String, bool> list) async {
|
Future<void> removeAllRatchets(String jid) async {
|
||||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
await ensureInitialized();
|
||||||
|
await _omemoManager.removeAllRatchets(jid);
|
||||||
// ignore: cascade_invocations
|
|
||||||
batch.delete(omemoTrustEnableListTable);
|
|
||||||
for (final entry in list.entries) {
|
|
||||||
batch.insert(
|
|
||||||
omemoTrustEnableListTable,
|
|
||||||
{
|
|
||||||
'key': entry.key,
|
|
||||||
'enabled': boolToInt(entry.value),
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await batch.commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<int>>> _loadTrustDeviceList() async {
|
Future<OmemoDevice> getDevice() async {
|
||||||
final entries = await GetIt.I
|
await ensureInitialized();
|
||||||
.get<DatabaseService>()
|
return _omemoManager.getDevice();
|
||||||
.database
|
|
||||||
.query(omemoTrustDeviceListTable);
|
|
||||||
|
|
||||||
final map = <String, List<int>>{};
|
|
||||||
for (final entry in entries) {
|
|
||||||
final key = entry['jid']! as String;
|
|
||||||
final device = entry['device']! as int;
|
|
||||||
|
|
||||||
if (map.containsKey(key)) {
|
|
||||||
map[key]!.add(device);
|
|
||||||
} else {
|
|
||||||
map[key] = [device];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveTrustDeviceList(Map<String, List<int>> list) async {
|
Future<model.OmemoDevice> regenerateDevice() async {
|
||||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
await ensureInitialized();
|
||||||
|
|
||||||
// ignore: cascade_invocations
|
final oldDeviceId = (await getDevice()).id;
|
||||||
batch.delete(omemoTrustDeviceListTable);
|
|
||||||
for (final entry in list.entries) {
|
|
||||||
for (final device in entry.value) {
|
|
||||||
batch.insert(
|
|
||||||
omemoTrustDeviceListTable,
|
|
||||||
{
|
|
||||||
'jid': entry.key,
|
|
||||||
'device': device,
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await batch.commit();
|
// Generate the new device
|
||||||
}
|
final newDevice = await _omemoManager.regenerateDevice();
|
||||||
|
|
||||||
Future<void> _saveOmemoDevice(OmemoDevice device) async {
|
// Remove the old device
|
||||||
await GetIt.I.get<DatabaseService>().database.insert(
|
unawaited(
|
||||||
omemoDeviceTable,
|
GetIt.I
|
||||||
{
|
.get<moxxmpp.XmppConnection>()
|
||||||
'jid': device.jid,
|
.getManagerById<moxxmpp.OmemoManager>(moxxmpp.omemoManager)!
|
||||||
'id': device.id,
|
.deleteDevice(oldDeviceId),
|
||||||
'data': jsonEncode(await device.toJson()),
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<OmemoDevice?> _loadOmemoDevice(String jid) async {
|
|
||||||
final data = await GetIt.I.get<DatabaseService>().database.query(
|
|
||||||
omemoDeviceTable,
|
|
||||||
where: 'jid = ?',
|
|
||||||
whereArgs: [jid],
|
|
||||||
limit: 1,
|
|
||||||
);
|
|
||||||
if (data.isEmpty) return null;
|
|
||||||
|
|
||||||
final deviceJson =
|
|
||||||
jsonDecode(data.first['data']! as String) as Map<String, dynamic>;
|
|
||||||
// NOTE: We need to do this because Dart otherwise complains about not being able
|
|
||||||
// to cast dynamic to List<int>.
|
|
||||||
final opks = List<Map<String, dynamic>>.empty(growable: true);
|
|
||||||
final opksIter = deviceJson['opks']! as List<dynamic>;
|
|
||||||
for (final tmpOpk in opksIter) {
|
|
||||||
final opk = tmpOpk as Map<String, dynamic>;
|
|
||||||
opks.add(<String, dynamic>{
|
|
||||||
'id': opk['id']! as int,
|
|
||||||
'public': opk['public']! as String,
|
|
||||||
'private': opk['private']! as String,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
deviceJson['opks'] = opks;
|
|
||||||
return OmemoDevice.fromJson(deviceJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, List<int>>> _loadOmemoDeviceList() async {
|
|
||||||
final list = await GetIt.I
|
|
||||||
.get<DatabaseService>()
|
|
||||||
.database
|
|
||||||
.query(omemoDeviceListTable);
|
|
||||||
final map = <String, List<int>>{};
|
|
||||||
for (final entry in list) {
|
|
||||||
final key = entry['jid']! as String;
|
|
||||||
final id = entry['id']! as int;
|
|
||||||
|
|
||||||
if (map.containsKey(key)) {
|
|
||||||
map[key]!.add(id);
|
|
||||||
} else {
|
|
||||||
map[key] = [id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveOmemoDeviceList(Map<String, List<int>> list) async {
|
|
||||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
|
||||||
|
|
||||||
// ignore: cascade_invocations
|
|
||||||
batch.delete(omemoDeviceListTable);
|
|
||||||
for (final entry in list.entries) {
|
|
||||||
for (final id in entry.value) {
|
|
||||||
batch.insert(
|
|
||||||
omemoDeviceListTable,
|
|
||||||
{
|
|
||||||
'jid': entry.key,
|
|
||||||
'id': id,
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await batch.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _emptyOmemoSessionTables() async {
|
|
||||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
|
||||||
|
|
||||||
// ignore: cascade_invocations
|
|
||||||
batch
|
|
||||||
..delete(omemoRatchetsTable)
|
|
||||||
..delete(omemoTrustCacheTable)
|
|
||||||
..delete(omemoTrustEnableListTable);
|
|
||||||
|
|
||||||
await batch.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _addFingerprintsToCache(List<OmemoCacheTriple> items) async {
|
|
||||||
final batch = GetIt.I.get<DatabaseService>().database.batch();
|
|
||||||
for (final item in items) {
|
|
||||||
batch.insert(
|
|
||||||
omemoFingerprintCache,
|
|
||||||
<String, dynamic>{
|
|
||||||
'jid': item.jid,
|
|
||||||
'id': item.deviceId,
|
|
||||||
'fingerprint': item.fingerprint,
|
|
||||||
},
|
|
||||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await batch.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<OmemoCacheTriple>> _getFingerprintsFromCache(String jid) async {
|
|
||||||
final rawItems = await GetIt.I.get<DatabaseService>().database.query(
|
|
||||||
omemoFingerprintCache,
|
|
||||||
where: 'jid = ?',
|
|
||||||
whereArgs: [jid],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return rawItems.map((item) {
|
return model.OmemoDevice(
|
||||||
return OmemoCacheTriple(
|
await newDevice.getFingerprint(),
|
||||||
jid,
|
true,
|
||||||
item['id']! as int,
|
true,
|
||||||
item['fingerprint']! as String,
|
true,
|
||||||
);
|
newDevice.id,
|
||||||
}).toList();
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a pseudo-message of type [type] to the chat with [conversationJid].
|
||||||
|
/// Also sends an event to the UI.
|
||||||
|
Future<void> addPseudoMessage(
|
||||||
|
String conversationJid,
|
||||||
|
PseudoMessageType type,
|
||||||
|
int ratchetsAdded,
|
||||||
|
int ratchetsReplaced,
|
||||||
|
) async {
|
||||||
|
final ms = GetIt.I.get<MessageService>();
|
||||||
|
final message = await ms.addMessageFromData(
|
||||||
|
'',
|
||||||
|
DateTime.now().millisecondsSinceEpoch,
|
||||||
|
'',
|
||||||
|
conversationJid,
|
||||||
|
'',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
pseudoMessageType: type,
|
||||||
|
pseudoMessageData: {
|
||||||
|
'ratchetsAdded': ratchetsAdded,
|
||||||
|
'ratchetsReplaced': ratchetsReplaced,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sendEvent(
|
||||||
|
MessageAddedEvent(
|
||||||
|
message: message,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
308
lib/service/omemo/persistence.dart
Normal file
308
lib/service/omemo/persistence.dart
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:cryptography/cryptography.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:omemo_dart/omemo_dart.dart';
|
||||||
|
import 'package:sqflite_common/sql.dart';
|
||||||
|
|
||||||
|
extension ByteListHelpers on List<int> {
|
||||||
|
String toBase64() {
|
||||||
|
return base64Encode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
OmemoPublicKey toPublicKey(KeyPairType type) {
|
||||||
|
return OmemoPublicKey.fromBytes(this, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> commitDevice(OmemoDevice device) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
final serializedOpks = <String, Map<String, String>>{};
|
||||||
|
for (final entry in device.opks.entries) {
|
||||||
|
serializedOpks[entry.key.toString()] = {
|
||||||
|
'public': base64Encode(await entry.value.pk.getBytes()),
|
||||||
|
'private': base64Encode(await entry.value.sk.getBytes()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(
|
||||||
|
omemoDevicesTable,
|
||||||
|
{
|
||||||
|
'jid': device.jid,
|
||||||
|
'id': device.id,
|
||||||
|
'ikPub': base64Encode(await device.ik.pk.getBytes()),
|
||||||
|
'ik': base64Encode(await device.ik.sk.getBytes()),
|
||||||
|
'spkPub': base64Encode(await device.spk.pk.getBytes()),
|
||||||
|
'spk': base64Encode(await device.spk.sk.getBytes()),
|
||||||
|
'spkId': device.spkId,
|
||||||
|
'spkSig': base64Encode(device.spkSignature),
|
||||||
|
'oldSpkPub': (await device.oldSpk?.pk.getBytes())?.toBase64(),
|
||||||
|
'oldSpk': (await device.oldSpk?.sk.getBytes())?.toBase64(),
|
||||||
|
'oldSpkId': device.oldSpkId,
|
||||||
|
'opks': jsonEncode(serializedOpks),
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OmemoDevice?> loadOmemoDevice(String jid) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
final rawDevice = await db.query(
|
||||||
|
omemoDevicesTable,
|
||||||
|
where: 'jid = ?',
|
||||||
|
whereArgs: [jid],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
if (rawDevice.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deviceJson = rawDevice.first;
|
||||||
|
|
||||||
|
// Deserialize the OPKs first
|
||||||
|
final deserializedOpks = <int, OmemoKeyPair>{};
|
||||||
|
final opks =
|
||||||
|
(jsonDecode(rawDevice.first['opks']! as String) as Map<dynamic, dynamic>)
|
||||||
|
.cast<String, dynamic>();
|
||||||
|
for (final opk in opks.entries) {
|
||||||
|
final opkValue = (opk.value as Map<String, dynamic>).cast<String, String>();
|
||||||
|
deserializedOpks[int.parse(opk.key)] = OmemoKeyPair.fromBytes(
|
||||||
|
base64Decode(opkValue['public']!),
|
||||||
|
base64Decode(opkValue['private']!),
|
||||||
|
KeyPairType.x25519,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OmemoKeyPair? oldSpk;
|
||||||
|
if (deviceJson['oldSpkPub'] != null && deviceJson['oldSpk'] != null) {
|
||||||
|
oldSpk = OmemoKeyPair.fromBytes(
|
||||||
|
base64Decode(deviceJson['oldSpkPub']! as String),
|
||||||
|
base64Decode(deviceJson['oldSpk']! as String),
|
||||||
|
KeyPairType.x25519,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OmemoDevice(
|
||||||
|
jid,
|
||||||
|
deviceJson['id']! as int,
|
||||||
|
OmemoKeyPair.fromBytes(
|
||||||
|
base64Decode(deviceJson['ikPub']! as String),
|
||||||
|
base64Decode(deviceJson['ik']! as String),
|
||||||
|
KeyPairType.ed25519,
|
||||||
|
),
|
||||||
|
OmemoKeyPair.fromBytes(
|
||||||
|
base64Decode(deviceJson['spkPub']! as String),
|
||||||
|
base64Decode(deviceJson['spk']! as String),
|
||||||
|
KeyPairType.x25519,
|
||||||
|
),
|
||||||
|
deviceJson['spkId']! as int,
|
||||||
|
base64Decode(deviceJson['spkSig']! as String),
|
||||||
|
oldSpk,
|
||||||
|
deviceJson['oldSpkId'] as int?,
|
||||||
|
deserializedOpks,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> commitRatchets(List<OmemoRatchetData> ratchets) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
final batch = db.batch();
|
||||||
|
for (final ratchet in ratchets) {
|
||||||
|
// Serialize the skipped keys
|
||||||
|
final serializedSkippedKeys = <Map<String, Object>>[];
|
||||||
|
for (final sk in ratchet.ratchet.mkSkipped.entries) {
|
||||||
|
serializedSkippedKeys.add({
|
||||||
|
'dhPub': (await sk.key.dh.getBytes()).toBase64(),
|
||||||
|
'n': sk.key.n,
|
||||||
|
'mk': sk.value.toBase64(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the KEX
|
||||||
|
final kex = {
|
||||||
|
'pkId': ratchet.ratchet.kex.pkId,
|
||||||
|
'spkId': ratchet.ratchet.kex.spkId,
|
||||||
|
'ek': (await ratchet.ratchet.kex.ek.getBytes()).toBase64(),
|
||||||
|
'ik': (await ratchet.ratchet.kex.ik.getBytes()).toBase64(),
|
||||||
|
};
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
omemoRatchetsTable,
|
||||||
|
{
|
||||||
|
'jid': ratchet.jid,
|
||||||
|
'device': ratchet.id,
|
||||||
|
'dhsPub': base64Encode(await ratchet.ratchet.dhs.pk.getBytes()),
|
||||||
|
'dhs': base64Encode(await ratchet.ratchet.dhs.sk.getBytes()),
|
||||||
|
'dhrPub': (await ratchet.ratchet.dhr?.getBytes())?.toBase64(),
|
||||||
|
'rk': base64Encode(ratchet.ratchet.rk),
|
||||||
|
'cks': ratchet.ratchet.cks?.toBase64(),
|
||||||
|
'ckr': ratchet.ratchet.ckr?.toBase64(),
|
||||||
|
'ns': ratchet.ratchet.ns,
|
||||||
|
'nr': ratchet.ratchet.nr,
|
||||||
|
'pn': ratchet.ratchet.pn,
|
||||||
|
'ik': (await ratchet.ratchet.ik.getBytes()).toBase64(),
|
||||||
|
'ad': ratchet.ratchet.sessionAd.toBase64(),
|
||||||
|
'skipped': jsonEncode(serializedSkippedKeys),
|
||||||
|
'kex': jsonEncode(kex),
|
||||||
|
'acked': boolToInt(ratchet.ratchet.acknowledged),
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> commitDeviceList(String jid, List<int> devices) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
await db.insert(
|
||||||
|
omemoDeviceListTable,
|
||||||
|
{
|
||||||
|
'jid': jid,
|
||||||
|
'devices': jsonEncode(devices),
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeRatchets(List<RatchetMapKey> ratchets) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
final batch = db.batch();
|
||||||
|
|
||||||
|
for (final key in ratchets) {
|
||||||
|
batch.delete(
|
||||||
|
omemoRatchetsTable,
|
||||||
|
where: 'jid = ? AND device = ?',
|
||||||
|
whereArgs: [key.jid, key.deviceId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OmemoDataPackage?> loadRatchets(String jid) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
final ratchetsRaw = await db.query(
|
||||||
|
omemoRatchetsTable,
|
||||||
|
where: 'jid = ?',
|
||||||
|
whereArgs: [jid],
|
||||||
|
);
|
||||||
|
final deviceListRaw = await db.query(
|
||||||
|
omemoDeviceListTable,
|
||||||
|
where: 'jid = ?',
|
||||||
|
whereArgs: [jid],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
if (ratchetsRaw.isEmpty || deviceListRaw.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the ratchets
|
||||||
|
final ratchets = <RatchetMapKey, OmemoDoubleRatchet>{};
|
||||||
|
for (final ratchetRaw in ratchetsRaw) {
|
||||||
|
final key = RatchetMapKey(
|
||||||
|
jid,
|
||||||
|
ratchetRaw['device']! as int,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deserialize skipped keys
|
||||||
|
final mkSkipped = <SkippedKey, List<int>>{};
|
||||||
|
final skippedKeysRaw =
|
||||||
|
(jsonDecode(ratchetRaw['skipped']! as String) as List<dynamic>)
|
||||||
|
.cast<Map<dynamic, dynamic>>();
|
||||||
|
for (final skippedRaw in skippedKeysRaw) {
|
||||||
|
final key = SkippedKey(
|
||||||
|
(skippedRaw['dhPub']! as String)
|
||||||
|
.fromBase64()
|
||||||
|
.toPublicKey(KeyPairType.x25519),
|
||||||
|
skippedRaw['n']! as int,
|
||||||
|
);
|
||||||
|
mkSkipped[key] = (skippedRaw['mk']! as String).fromBase64();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize the KEX
|
||||||
|
final kexRaw =
|
||||||
|
(jsonDecode(ratchetRaw['kex']! as String) as Map<dynamic, dynamic>)
|
||||||
|
.cast<String, Object>();
|
||||||
|
final kex = KeyExchangeData(
|
||||||
|
kexRaw['pkId']! as int,
|
||||||
|
kexRaw['spkId']! as int,
|
||||||
|
(kexRaw['ek']! as String).fromBase64().toPublicKey(KeyPairType.x25519),
|
||||||
|
(kexRaw['ik']! as String).fromBase64().toPublicKey(KeyPairType.ed25519),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deserialize the entire ratchet
|
||||||
|
ratchets[key] = OmemoDoubleRatchet(
|
||||||
|
OmemoKeyPair.fromBytes(
|
||||||
|
base64Decode(ratchetRaw['dhsPub']! as String),
|
||||||
|
base64Decode(ratchetRaw['dhs']! as String),
|
||||||
|
KeyPairType.x25519,
|
||||||
|
),
|
||||||
|
(ratchetRaw['dhrPub'] as String?)
|
||||||
|
?.fromBase64()
|
||||||
|
.toPublicKey(KeyPairType.x25519),
|
||||||
|
base64Decode(ratchetRaw['rk']! as String),
|
||||||
|
(ratchetRaw['cks'] as String?)?.fromBase64(),
|
||||||
|
(ratchetRaw['ckr'] as String?)?.fromBase64(),
|
||||||
|
ratchetRaw['ns']! as int,
|
||||||
|
ratchetRaw['nr']! as int,
|
||||||
|
ratchetRaw['pn']! as int,
|
||||||
|
(ratchetRaw['ik']! as String)
|
||||||
|
.fromBase64()
|
||||||
|
.toPublicKey(KeyPairType.ed25519),
|
||||||
|
(ratchetRaw['ad']! as String).fromBase64(),
|
||||||
|
mkSkipped,
|
||||||
|
intToBool(ratchetRaw['acked']! as int),
|
||||||
|
kex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OmemoDataPackage(
|
||||||
|
(jsonDecode(deviceListRaw.first['devices']! as String) as List<dynamic>)
|
||||||
|
.cast<int>(),
|
||||||
|
ratchets,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> commitTrust(BTBVTrustData trust) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
await db.insert(
|
||||||
|
omemoTrustTable,
|
||||||
|
{
|
||||||
|
'jid': trust.jid,
|
||||||
|
'device': trust.device,
|
||||||
|
'trust': trust.state.value,
|
||||||
|
'enabled': boolToInt(trust.enabled),
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BTBVTrustData>> loadTrust(String jid) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
final rawTrust = await db.query(
|
||||||
|
omemoTrustTable,
|
||||||
|
where: 'jid = ?',
|
||||||
|
whereArgs: [jid],
|
||||||
|
);
|
||||||
|
|
||||||
|
return rawTrust.map((trust) {
|
||||||
|
return BTBVTrustData(
|
||||||
|
jid,
|
||||||
|
trust['device']! as int,
|
||||||
|
BTBVTrustState.fromInt(trust['trust']! as int),
|
||||||
|
intToBool(trust['enabled']! as int),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeTrust(String jid) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
await db.delete(
|
||||||
|
omemoTrustTable,
|
||||||
|
where: 'jid = ?',
|
||||||
|
whereArgs: [jid],
|
||||||
|
);
|
||||||
|
}
|
@ -23,7 +23,6 @@ import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
|||||||
import 'package:moxxyv2/service/language.dart';
|
import 'package:moxxyv2/service/language.dart';
|
||||||
import 'package:moxxyv2/service/message.dart';
|
import 'package:moxxyv2/service/message.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/connectivity.dart';
|
import 'package:moxxyv2/service/moxxmpp/connectivity.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/omemo.dart';
|
|
||||||
import 'package:moxxyv2/service/moxxmpp/roster.dart';
|
import 'package:moxxyv2/service/moxxmpp/roster.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/socket.dart';
|
import 'package:moxxyv2/service/moxxmpp/socket.dart';
|
||||||
import 'package:moxxyv2/service/moxxmpp/stream.dart';
|
import 'package:moxxyv2/service/moxxmpp/stream.dart';
|
||||||
@ -222,7 +221,12 @@ Future<void> entrypoint() async {
|
|||||||
const Identity(category: 'client', type: 'phone', name: 'Moxxy'),
|
const Identity(category: 'client', type: 'phone', name: 'Moxxy'),
|
||||||
]),
|
]),
|
||||||
RosterManager(MoxxyRosterStateManager()),
|
RosterManager(MoxxyRosterStateManager()),
|
||||||
MoxxyOmemoManager(),
|
OmemoManager(
|
||||||
|
GetIt.I.get<OmemoService>().getOmemoManager,
|
||||||
|
(toJid, _) async => GetIt.I
|
||||||
|
.get<ConversationService>()
|
||||||
|
.shouldEncryptForConversation(toJid),
|
||||||
|
),
|
||||||
PingManager(const Duration(minutes: 3)),
|
PingManager(const Duration(minutes: 3)),
|
||||||
MessageManager(),
|
MessageManager(),
|
||||||
PresenceManager(),
|
PresenceManager(),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@ -313,8 +314,7 @@ class XmppService {
|
|||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
|
|
||||||
final hasUrlSource = firstWhereOrNull(
|
final hasUrlSource = sfs!.sources.firstWhereOrNull(
|
||||||
sfs!.sources,
|
|
||||||
(src) => src is StatelessFileSharingUrlSource,
|
(src) => src is StatelessFileSharingUrlSource,
|
||||||
) !=
|
) !=
|
||||||
null;
|
null;
|
||||||
@ -336,8 +336,7 @@ class XmppService {
|
|||||||
sfs.metadata.size,
|
sfs.metadata.size,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final encryptedSource = firstWhereOrNull(
|
final encryptedSource = sfs.sources.firstWhereOrNull(
|
||||||
sfs.sources,
|
|
||||||
(src) => src is StatelessFileSharingEncryptedSource,
|
(src) => src is StatelessFileSharingEncryptedSource,
|
||||||
)! as StatelessFileSharingEncryptedSource;
|
)! as StatelessFileSharingEncryptedSource;
|
||||||
|
|
||||||
@ -387,22 +386,24 @@ class XmppService {
|
|||||||
final manager = GetIt.I
|
final manager = GetIt.I
|
||||||
.get<XmppConnection>()
|
.get<XmppConnection>()
|
||||||
.getManagerById<MessageManager>(messageManager)!;
|
.getManagerById<MessageManager>(messageManager)!;
|
||||||
if (isMarkable && info.features.contains(chatMarkersXmlns)) {
|
final hasId = originId != null || event.id != null;
|
||||||
|
if (isMarkable && info.features.contains(chatMarkersXmlns) && hasId) {
|
||||||
await manager.sendMessage(
|
await manager.sendMessage(
|
||||||
event.from.toBare(),
|
event.from.toBare(),
|
||||||
TypedMap<StanzaHandlerExtension>.fromList([
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
ChatMarkerData(
|
ChatMarkerData(
|
||||||
ChatMarker.received,
|
ChatMarker.received,
|
||||||
originId ?? event.id,
|
originId ?? event.id!,
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
} else if (deliveryReceiptRequested &&
|
} else if (deliveryReceiptRequested &&
|
||||||
info.features.contains(deliveryXmlns)) {
|
info.features.contains(deliveryXmlns) &&
|
||||||
|
hasId) {
|
||||||
await manager.sendMessage(
|
await manager.sendMessage(
|
||||||
event.from.toBare(),
|
event.from.toBare(),
|
||||||
TypedMap<StanzaHandlerExtension>.fromList([
|
TypedMap<StanzaHandlerExtension>.fromList([
|
||||||
MessageDeliveryReceivedData(originId ?? event.id),
|
MessageDeliveryReceivedData(originId ?? event.id!),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -780,7 +781,9 @@ class XmppService {
|
|||||||
GetIt.I.get<BlocklistService>().onNewConnection();
|
GetIt.I.get<BlocklistService>().onNewConnection();
|
||||||
|
|
||||||
// Reset the OMEMO cache
|
// Reset the OMEMO cache
|
||||||
GetIt.I.get<OmemoService>().onNewConnection();
|
unawaited(
|
||||||
|
GetIt.I.get<OmemoService>().onNewConnection(),
|
||||||
|
);
|
||||||
|
|
||||||
// Enable carbons, if they're not already enabled (e.g. by using SASL2)
|
// Enable carbons, if they're not already enabled (e.g. by using SASL2)
|
||||||
final cm = connection.getManagerById<CarbonsManager>(carbonsManager)!;
|
final cm = connection.getManagerById<CarbonsManager>(carbonsManager)!;
|
||||||
@ -1054,10 +1057,17 @@ class XmppService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.id == null) {
|
||||||
|
_log.warning(
|
||||||
|
'Received error message without id.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
final msg = await ms.getMessageByStanzaId(
|
final msg = await ms.getMessageByStanzaId(
|
||||||
event.from.toBare().toString(),
|
event.from.toBare().toString(),
|
||||||
event.id,
|
event.id!,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (msg == null) {
|
if (msg == null) {
|
||||||
@ -1214,7 +1224,7 @@ class XmppService {
|
|||||||
|
|
||||||
// Stop the processing here if the event does not describe a displayable message
|
// Stop the processing here if the event does not describe a displayable message
|
||||||
if (!_isMessageEventMessage(event) && event.encryptionError == null) return;
|
if (!_isMessageEventMessage(event) && event.encryptionError == null) return;
|
||||||
if (event.encryptionError is InvalidKeyExchangeException) return;
|
if (event.encryptionError is InvalidKeyExchangeSignatureError) return;
|
||||||
|
|
||||||
// Ignore File Upload Notifications where we don't have a filename.
|
// Ignore File Upload Notifications where we don't have a filename.
|
||||||
final fun = event.extensions.get<FileUploadNotificationData>();
|
final fun = event.extensions.get<FileUploadNotificationData>();
|
||||||
@ -1234,8 +1244,6 @@ class XmppService {
|
|||||||
final isInRoster = rosterItem != null;
|
final isInRoster = rosterItem != null;
|
||||||
// True if the message was sent by us (via a Carbon)
|
// True if the message was sent by us (via a Carbon)
|
||||||
final sent = isCarbon && event.from.toBare().toString() == state.jid;
|
final sent = isCarbon && event.from.toBare().toString() == state.jid;
|
||||||
// The timestamp at which we received the message
|
|
||||||
final messageTimestamp = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
|
|
||||||
// Acknowledge the message if enabled
|
// Acknowledge the message if enabled
|
||||||
final receiptRequested =
|
final receiptRequested =
|
||||||
@ -1294,14 +1302,60 @@ class XmppService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log encryption errors
|
||||||
|
if (event.encryptionError != null) {
|
||||||
|
_log.warning(
|
||||||
|
'Got encryption error from moxxmpp for message: ${event.encryptionError}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have to create pseudo-messages related to OMEMO
|
||||||
|
final omemoData = event.get<OmemoData>();
|
||||||
|
if (omemoData != null) {
|
||||||
|
final addedRatchetsList =
|
||||||
|
omemoData.newRatchets.values.map((ids) => ids.length);
|
||||||
|
final amountAdded = addedRatchetsList.isEmpty
|
||||||
|
? 0
|
||||||
|
: addedRatchetsList.reduce((value, element) => value + element);
|
||||||
|
final replacedRatchetsList =
|
||||||
|
omemoData.replacedRatchets.values.map((ids) => ids.length);
|
||||||
|
final amountReplaced = replacedRatchetsList.isEmpty
|
||||||
|
? 0
|
||||||
|
: replacedRatchetsList.reduce((value, element) => value + element);
|
||||||
|
|
||||||
|
// Notify of new ratchets
|
||||||
|
final om = GetIt.I.get<OmemoService>();
|
||||||
|
if (omemoData.newRatchets.isNotEmpty) {
|
||||||
|
await om.addPseudoMessage(
|
||||||
|
conversationJid,
|
||||||
|
PseudoMessageType.newDevice,
|
||||||
|
amountAdded,
|
||||||
|
amountReplaced,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify of changed ratchets
|
||||||
|
if (omemoData.replacedRatchets.isNotEmpty) {
|
||||||
|
await om.addPseudoMessage(
|
||||||
|
conversationJid,
|
||||||
|
PseudoMessageType.changedDevice,
|
||||||
|
amountAdded,
|
||||||
|
amountReplaced,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the message in the database
|
// Create the message in the database
|
||||||
|
// The timestamp at which we received the message
|
||||||
|
final messageTimestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final ms = GetIt.I.get<MessageService>();
|
final ms = GetIt.I.get<MessageService>();
|
||||||
var message = await ms.addMessageFromData(
|
var message = await ms.addMessageFromData(
|
||||||
messageBody,
|
messageBody,
|
||||||
messageTimestamp,
|
messageTimestamp,
|
||||||
event.from.toString(),
|
event.from.toString(),
|
||||||
conversationJid,
|
conversationJid,
|
||||||
event.id,
|
// TODO(Unknown): Should we handle this differently?
|
||||||
|
event.id ?? '',
|
||||||
fun != null,
|
fun != null,
|
||||||
event.encrypted,
|
event.encrypted,
|
||||||
event.extensions
|
event.extensions
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:moxlib/awaitabledatasender.dart';
|
import 'package:moxlib/moxlib.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/shared/models/message.dart';
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
import 'package:moxxyv2/shared/models/preferences.dart';
|
import 'package:moxxyv2/shared/models/preferences.dart';
|
||||||
|
@ -25,11 +25,9 @@ int errorTypeFromException(dynamic exception) {
|
|||||||
return noError;
|
return noError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception is NoDecryptionKeyException) {
|
if (exception is InvalidMessageHMACError) {
|
||||||
return messageNoDecryptionKey;
|
|
||||||
} else if (exception is InvalidMessageHMACException) {
|
|
||||||
return messageInvalidHMAC;
|
return messageInvalidHMAC;
|
||||||
} else if (exception is NotEncryptedForDeviceException) {
|
} else if (exception is NotEncryptedForDeviceError) {
|
||||||
return messageNoDecryptionKey;
|
return messageNoDecryptionKey;
|
||||||
} else if (exception is InvalidAffixElementsException) {
|
} else if (exception is InvalidAffixElementsException) {
|
||||||
return messageInvalidAffixElements;
|
return messageInvalidAffixElements;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:moxlib/awaitabledatasender.dart';
|
import 'package:moxlib/moxlib.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||||
import 'package:moxxyv2/shared/models/message.dart';
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
|
@ -10,7 +10,30 @@ import 'package:moxxyv2/shared/warning_types.dart';
|
|||||||
part 'message.freezed.dart';
|
part 'message.freezed.dart';
|
||||||
part 'message.g.dart';
|
part 'message.g.dart';
|
||||||
|
|
||||||
const pseudoMessageTypeNewDevice = 1;
|
extension PseudoMessageTypeFromInt on int {
|
||||||
|
PseudoMessageType? toPseudoMessageType() {
|
||||||
|
switch (this) {
|
||||||
|
case 1:
|
||||||
|
return PseudoMessageType.newDevice;
|
||||||
|
case 2:
|
||||||
|
return PseudoMessageType.changedDevice;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PseudoMessageType {
|
||||||
|
/// Indicates that a new device was created in the chat.
|
||||||
|
newDevice(1),
|
||||||
|
|
||||||
|
/// Indicates that an existing device has been replaced.
|
||||||
|
changedDevice(2);
|
||||||
|
|
||||||
|
const PseudoMessageType(this.id);
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _optionalJsonDecodeWithFallback(String? data) {
|
Map<String, dynamic> _optionalJsonDecodeWithFallback(String? data) {
|
||||||
if (data == null) return <String, dynamic>{};
|
if (data == null) return <String, dynamic>{};
|
||||||
@ -53,7 +76,7 @@ class Message with _$Message {
|
|||||||
Message? quotes,
|
Message? quotes,
|
||||||
@Default([]) List<String> reactionsPreview,
|
@Default([]) List<String> reactionsPreview,
|
||||||
String? stickerPackId,
|
String? stickerPackId,
|
||||||
int? pseudoMessageType,
|
PseudoMessageType? pseudoMessageType,
|
||||||
Map<String, dynamic>? pseudoMessageData,
|
Map<String, dynamic>? pseudoMessageData,
|
||||||
}) = _Message;
|
}) = _Message;
|
||||||
|
|
||||||
@ -83,6 +106,13 @@ class Message with _$Message {
|
|||||||
'isEdited': intToBool(json['isEdited']! as int),
|
'isEdited': intToBool(json['isEdited']! as int),
|
||||||
'containsNoStore': intToBool(json['containsNoStore']! as int),
|
'containsNoStore': intToBool(json['containsNoStore']! as int),
|
||||||
'reactionsPreview': reactionsPreview,
|
'reactionsPreview': reactionsPreview,
|
||||||
|
// NOTE: freezed expects the field name of the enum value here and refused to accept
|
||||||
|
// actual enum value. Makes sense since we have to serialize it, I guess.
|
||||||
|
'pseudoMessageType': (json['pseudoMessageType'] as int?)
|
||||||
|
?.toPseudoMessageType()
|
||||||
|
.toString()
|
||||||
|
.split('.')
|
||||||
|
.last,
|
||||||
'pseudoMessageData':
|
'pseudoMessageData':
|
||||||
_optionalJsonDecodeWithFallback(json['pseudoMessageData'] as String?)
|
_optionalJsonDecodeWithFallback(json['pseudoMessageData'] as String?)
|
||||||
}).copyWith(
|
}).copyWith(
|
||||||
@ -114,6 +144,7 @@ class Message with _$Message {
|
|||||||
'isRetracted': boolToInt(isRetracted),
|
'isRetracted': boolToInt(isRetracted),
|
||||||
'isEdited': boolToInt(isEdited),
|
'isEdited': boolToInt(isEdited),
|
||||||
'containsNoStore': boolToInt(containsNoStore),
|
'containsNoStore': boolToInt(containsNoStore),
|
||||||
|
'pseudoMessageType': pseudoMessageType?.id,
|
||||||
'pseudoMessageData': _optionalJsonEncodeWithFallback(pseudoMessageData),
|
'pseudoMessageData': _optionalJsonEncodeWithFallback(pseudoMessageData),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,8 @@ class OmemoDevice with _$OmemoDevice {
|
|||||||
bool trusted,
|
bool trusted,
|
||||||
bool verified,
|
bool verified,
|
||||||
bool enabled,
|
bool enabled,
|
||||||
int deviceId, {
|
int deviceId,
|
||||||
@Default(true) bool hasSessionWith,
|
) = _OmemoDevice;
|
||||||
}) = _OmemoDevice;
|
|
||||||
|
|
||||||
/// JSON
|
/// JSON
|
||||||
factory OmemoDevice.fromJson(Map<String, dynamic> json) =>
|
factory OmemoDevice.fromJson(Map<String, dynamic> json) =>
|
||||||
|
@ -5,13 +5,27 @@ import 'package:moxxmpp/moxxmpp.dart';
|
|||||||
part 'xmpp_state.freezed.dart';
|
part 'xmpp_state.freezed.dart';
|
||||||
part 'xmpp_state.g.dart';
|
part 'xmpp_state.g.dart';
|
||||||
|
|
||||||
|
extension StreamManagementStateToJson on StreamManagementState {
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'c2s': c2s,
|
||||||
|
's2c': s2c,
|
||||||
|
'streamResumptionLocation': streamResumptionLocation,
|
||||||
|
'streamResumptionId': streamResumptionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class StreamManagementStateConverter
|
class StreamManagementStateConverter
|
||||||
implements JsonConverter<StreamManagementState, Map<String, dynamic>> {
|
implements JsonConverter<StreamManagementState, Map<String, dynamic>> {
|
||||||
const StreamManagementStateConverter();
|
const StreamManagementStateConverter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
StreamManagementState fromJson(Map<String, dynamic> json) =>
|
StreamManagementState fromJson(Map<String, dynamic> json) =>
|
||||||
StreamManagementState.fromJson(json);
|
StreamManagementState(
|
||||||
|
json['c2s']! as int,
|
||||||
|
json['s2c']! as int,
|
||||||
|
streamResumptionLocation: json['streamResumptionLocation'] as String?,
|
||||||
|
streamResumptionId: json['streamResumptionId'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson(StreamManagementState state) => state.toJson();
|
Map<String, dynamic> toJson(StreamManagementState state) => state.toJson();
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||||
@ -47,8 +47,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
|||||||
) async {
|
) async {
|
||||||
final cb = GetIt.I.get<ConversationsBloc>();
|
final cb = GetIt.I.get<ConversationsBloc>();
|
||||||
await cb.waitUntilInitialized();
|
await cb.waitUntilInitialized();
|
||||||
final conversation = firstWhereOrNull(
|
final conversation = cb.state.conversations.firstWhereOrNull(
|
||||||
cb.state.conversations,
|
|
||||||
(Conversation c) => c.jid == event.jid,
|
(Conversation c) => c.jid == event.jid,
|
||||||
)!;
|
)!;
|
||||||
emit(
|
emit(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/service/database/helpers.dart';
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
@ -43,10 +43,11 @@ class NewConversationBloc
|
|||||||
final conversations = GetIt.I.get<ConversationsBloc>();
|
final conversations = GetIt.I.get<ConversationsBloc>();
|
||||||
|
|
||||||
// Guard against an unneccessary roundtrip
|
// Guard against an unneccessary roundtrip
|
||||||
if (listContains(
|
final listContains = conversations.state.conversations.firstWhereOrNull(
|
||||||
conversations.state.conversations,
|
(Conversation c) => c.jid == event.jid,
|
||||||
(Conversation c) => c.jid == event.jid,
|
) !=
|
||||||
)) {
|
null;
|
||||||
|
if (listContains) {
|
||||||
GetIt.I.get<conversation.ConversationBloc>().add(
|
GetIt.I.get<conversation.ConversationBloc>().add(
|
||||||
conversation.RequestedConversationEvent(
|
conversation.RequestedConversationEvent(
|
||||||
event.jid,
|
event.jid,
|
||||||
@ -120,8 +121,7 @@ class NewConversationBloc
|
|||||||
if (event.removed.contains(item.jid)) continue;
|
if (event.removed.contains(item.jid)) continue;
|
||||||
|
|
||||||
// Handle modified items
|
// Handle modified items
|
||||||
final modified = firstWhereOrNull(
|
final modified = event.modified.firstWhereOrNull(
|
||||||
event.modified,
|
|
||||||
(RosterItem i) => i.id == item.id,
|
(RosterItem i) => i.id == item.id,
|
||||||
);
|
);
|
||||||
if (modified != null) {
|
if (modified != null) {
|
||||||
|
@ -87,17 +87,6 @@ class OwnDevicesBloc extends Bloc<OwnDevicesEvent, OwnDevicesState> {
|
|||||||
RecreateSessionsCommand(jid: GetIt.I.get<UIDataService>().ownJid!),
|
RecreateSessionsCommand(jid: GetIt.I.get<UIDataService>().ownJid!),
|
||||||
awaitable: false,
|
awaitable: false,
|
||||||
);
|
);
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
keys: List.from(
|
|
||||||
state.keys.map(
|
|
||||||
(key) => key.copyWith(
|
|
||||||
hasSessionWith: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent());
|
GetIt.I.get<NavigationBloc>().add(PoppedRouteEvent());
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
@ -44,10 +44,13 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Apply
|
// Apply
|
||||||
final stickerPack = firstWhereOrNull(
|
final stickerPack = GetIt.I
|
||||||
GetIt.I.get<stickers.StickersBloc>().state.stickerPacks,
|
.get<stickers.StickersBloc>()
|
||||||
(StickerPack pack) => pack.id == event.stickerPackId,
|
.state
|
||||||
);
|
.stickerPacks
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(StickerPack pack) => pack.id == event.stickerPackId,
|
||||||
|
);
|
||||||
assert(stickerPack != null, 'The sticker pack must be found');
|
assert(stickerPack != null, 'The sticker pack must be found');
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -177,10 +180,13 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
|
|||||||
Emitter<StickerPackState> emit,
|
Emitter<StickerPackState> emit,
|
||||||
) async {
|
) async {
|
||||||
// Find out if the sticker pack is locally available or not
|
// Find out if the sticker pack is locally available or not
|
||||||
final stickerPack = firstWhereOrNull(
|
final stickerPack = GetIt.I
|
||||||
GetIt.I.get<stickers.StickersBloc>().state.stickerPacks,
|
.get<stickers.StickersBloc>()
|
||||||
(StickerPack pack) => pack.id == event.stickerPackId,
|
.state
|
||||||
);
|
.stickerPacks
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(StickerPack pack) => pack.id == event.stickerPackId,
|
||||||
|
);
|
||||||
|
|
||||||
if (stickerPack == null) {
|
if (stickerPack == null) {
|
||||||
await _onRemoteStickerPackRequested(
|
await _onRemoteStickerPackRequested(
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
@ -53,8 +53,7 @@ class StickersBloc extends Bloc<StickersEvent, StickersState> {
|
|||||||
StickerPackRemovedEvent event,
|
StickerPackRemovedEvent event,
|
||||||
Emitter<StickersState> emit,
|
Emitter<StickersState> emit,
|
||||||
) async {
|
) async {
|
||||||
final stickerPack = firstWhereOrNull(
|
final stickerPack = state.stickerPacks.firstWhereOrNull(
|
||||||
state.stickerPacks,
|
|
||||||
(StickerPack sp) => sp.id == event.stickerPackId,
|
(StickerPack sp) => sp.id == event.stickerPackId,
|
||||||
)!;
|
)!;
|
||||||
final sm = Map<StickerKey, Sticker>.from(state.stickerMap);
|
final sm = Map<StickerKey, Sticker>.from(state.stickerMap);
|
||||||
|
@ -3,7 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxlib/awaitabledatasender.dart';
|
import 'package:moxlib/moxlib.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
import 'package:moxxyv2/shared/eventhandler.dart';
|
import 'package:moxxyv2/shared/eventhandler.dart';
|
||||||
|
@ -23,8 +23,8 @@ import 'package:moxxyv2/ui/pages/conversation/topbar.dart';
|
|||||||
import 'package:moxxyv2/ui/pages/conversation/typing_indicator.dart';
|
import 'package:moxxyv2/ui/pages/conversation/typing_indicator.dart';
|
||||||
import 'package:moxxyv2/ui/service/data.dart';
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
import 'package:moxxyv2/ui/theme.dart';
|
import 'package:moxxyv2/ui/theme.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/bubbles/bubbles.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/bubbles/date.dart';
|
import 'package:moxxyv2/ui/widgets/chat/bubbles/date.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/chat/bubbles/new_device.dart';
|
|
||||||
import 'package:moxxyv2/ui/widgets/chat/chatbubble.dart';
|
import 'package:moxxyv2/ui/widgets/chat/chatbubble.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/combined_picker.dart';
|
import 'package:moxxyv2/ui/widgets/combined_picker.dart';
|
||||||
import 'package:moxxyv2/ui/widgets/context_menu.dart';
|
import 'package:moxxyv2/ui/widgets/context_menu.dart';
|
||||||
@ -232,10 +232,7 @@ class ConversationPageState extends State<ConversationPage>
|
|||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: maxWidth,
|
maxWidth: maxWidth,
|
||||||
),
|
),
|
||||||
child: NewDeviceBubble(
|
child: bubbleFromPseudoMessageType(context, item),
|
||||||
data: item.pseudoMessageData!,
|
|
||||||
title: state.conversation!.title,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -107,30 +107,25 @@ class OwnDevicesPage extends StatelessWidget {
|
|||||||
item.enabled,
|
item.enabled,
|
||||||
item.verified,
|
item.verified,
|
||||||
hasVerifiedDevices,
|
hasVerifiedDevices,
|
||||||
onVerifiedPressed: !item.hasSessionWith
|
onVerifiedPressed: () async {
|
||||||
? null
|
if (item.verified) return;
|
||||||
: () async {
|
|
||||||
if (item.verified) return;
|
|
||||||
if (!item.hasSessionWith) return;
|
|
||||||
|
|
||||||
final uri = await scanXmppUriQrCode(context);
|
final uri = await scanXmppUriQrCode(context);
|
||||||
if (uri == null) return;
|
if (uri == null) return;
|
||||||
|
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
context.read<OwnDevicesBloc>().add(
|
context.read<OwnDevicesBloc>().add(
|
||||||
DeviceVerifiedEvent(uri, item.deviceId),
|
DeviceVerifiedEvent(uri, item.deviceId),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onEnableValueChanged: !item.hasSessionWith
|
onEnableValueChanged: (value) {
|
||||||
? null
|
context.read<OwnDevicesBloc>().add(
|
||||||
: (value) {
|
OwnDeviceEnabledSetEvent(
|
||||||
context.read<OwnDevicesBloc>().add(
|
item.deviceId,
|
||||||
OwnDeviceEnabledSetEvent(
|
value,
|
||||||
item.deviceId,
|
),
|
||||||
value,
|
);
|
||||||
),
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
onDeletePressed: () async {
|
onDeletePressed: () async {
|
||||||
final result = await showConfirmationDialog(
|
final result = await showConfirmationDialog(
|
||||||
t.pages.profile.owndevices.deleteDeviceConfirmTitle,
|
t.pages.profile.owndevices.deleteDeviceConfirmTitle,
|
||||||
|
38
lib/ui/widgets/chat/bubbles/bubbles.dart
Normal file
38
lib/ui/widgets/chat/bubbles/bubbles.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/devices_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/widgets/chat/bubbles/omemo.dart';
|
||||||
|
|
||||||
|
Widget bubbleFromPseudoMessageType(BuildContext context, Message message) {
|
||||||
|
assert(
|
||||||
|
message.pseudoMessageType != null,
|
||||||
|
'Message must have non-null pseudoMessageType',
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (message.pseudoMessageType!) {
|
||||||
|
case PseudoMessageType.changedDevice:
|
||||||
|
final replacedAmount =
|
||||||
|
(message.pseudoMessageData?['ratchetsReplaced'] as int?) ?? 1;
|
||||||
|
return OmemoBubble(
|
||||||
|
text: t.pages.conversation.replacedDeviceMessage(n: replacedAmount),
|
||||||
|
onTap: () {
|
||||||
|
context.read<DevicesBloc>().add(
|
||||||
|
DevicesRequestedEvent(message.conversationJid),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case PseudoMessageType.newDevice:
|
||||||
|
final addedAmount =
|
||||||
|
(message.pseudoMessageData?['ratchetsAdded'] as int?) ?? 1;
|
||||||
|
return OmemoBubble(
|
||||||
|
text: t.pages.conversation.newDeviceMessage(n: addedAmount),
|
||||||
|
onTap: () {
|
||||||
|
context.read<DevicesBloc>().add(
|
||||||
|
DevicesRequestedEvent(message.conversationJid),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
|
||||||
import 'package:moxxyv2/ui/bloc/devices_bloc.dart';
|
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
|
|
||||||
class NewDeviceBubble extends StatelessWidget {
|
class OmemoBubble extends StatelessWidget {
|
||||||
const NewDeviceBubble({
|
const OmemoBubble({
|
||||||
required this.data,
|
required this.text,
|
||||||
required this.title,
|
required this.onTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
final Map<String, dynamic> data;
|
|
||||||
final String title;
|
/// The text to display in the bubble.
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
/// Callback for tapping the message.
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -22,18 +23,14 @@ class NewDeviceBubble extends StatelessWidget {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: bubbleColorNewDevice,
|
color: bubbleColorNewDevice,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: onTap,
|
||||||
context.read<DevicesBloc>().add(
|
|
||||||
DevicesRequestedEvent(data['jid']! as String),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
vertical: 6,
|
vertical: 6,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
t.pages.conversation.newDeviceMessage(title: title),
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
45
pubspec.lock
45
pubspec.lock
@ -921,40 +921,40 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: moxlib
|
name: moxlib
|
||||||
sha256: "135507494010803fba2d8a7333d323271c978559152882ca6ea695e5edbcd606"
|
sha256: "2a76a632d23ea73906964cee4463352995e40199036162217ea323a6c3846e73"
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.5"
|
version: "0.2.0"
|
||||||
moxplatform:
|
moxplatform:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: moxplatform
|
name: moxplatform
|
||||||
sha256: bd856b5b1cbdd45640aac22bc56747c5f1b0f3ca5b5e17767548e27a3f87eb2b
|
sha256: "6de28b4e358d09562f0c8275c565e0a25313003aa08501f1d6aa46350c1a1363"
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.15"
|
version: "0.1.16"
|
||||||
moxplatform_android:
|
moxplatform_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: moxplatform_android
|
name: moxplatform_android
|
||||||
sha256: "8b2ac2716afb970eb1a1af2a5fd5c6b8864ad08d44c85678d86fffdca27d373e"
|
sha256: f7af2e9f326dba6f712edc99aaa8b75f8b09332465f359f514333e79024c41de
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.15"
|
version: "0.1.16"
|
||||||
moxplatform_platform_interface:
|
moxplatform_platform_interface:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: moxplatform_platform_interface
|
name: moxplatform_platform_interface
|
||||||
sha256: "84bb5fb791567c1a197dba94dfad9e59e0839aa9a1e26359a70e0e9631ea71d6"
|
sha256: eba3f50a3fe00cd0d5fa9baae0f77a66017771c17ff1c376bd4c1ba04088bd5e
|
||||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.15"
|
version: "0.1.16"
|
||||||
moxxmpp:
|
moxxmpp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/moxxmpp"
|
path: "packages/moxxmpp"
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: fa2ce7c2d10042246e19249f672ce32f90bab087
|
resolved-ref: "05e3d804a4036e9cd93fd27473a1e970fda3c3fc"
|
||||||
url: "https://codeberg.org/moxxy/moxxmpp.git"
|
url: "https://codeberg.org/moxxy/moxxmpp.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.4.0"
|
version: "0.4.0"
|
||||||
@ -1009,11 +1009,12 @@ packages:
|
|||||||
omemo_dart:
|
omemo_dart:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: omemo_dart
|
path: "."
|
||||||
sha256: f255a0a16838b2a2373cf9739d2310909e79be179d894321b2c97d0531fc9614
|
ref: HEAD
|
||||||
url: "https://git.polynom.me/api/packages/PapaTutuWawa/pub/"
|
resolved-ref: "49c7e114e6cf80dcde55fbbd218bba3182045862"
|
||||||
source: hosted
|
url: "https://github.com/PapaTutuWawa/omemo_dart.git"
|
||||||
version: "0.4.3"
|
source: git
|
||||||
|
version: "0.5.1"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1198,6 +1199,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.4"
|
version: "4.2.4"
|
||||||
|
protobuf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protobuf
|
||||||
|
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
protoc_plugin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protoc_plugin
|
||||||
|
sha256: e2be5014ba145dc0f8de20ac425afa2a513aff64fe350d338e481d40de0573df
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "20.0.1"
|
||||||
provider:
|
provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
15
pubspec.yaml
15
pubspec.yaml
@ -62,13 +62,13 @@ dependencies:
|
|||||||
version: 0.1.4+1
|
version: 0.1.4+1
|
||||||
moxlib:
|
moxlib:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.1.5
|
version: ^0.2.0
|
||||||
moxplatform:
|
moxplatform:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.1.15
|
version: 0.1.16
|
||||||
moxplatform_platform_interface:
|
moxplatform_platform_interface:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.1.15
|
version: 0.1.16
|
||||||
moxxmpp:
|
moxxmpp:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
@ -81,7 +81,7 @@ dependencies:
|
|||||||
native_imaging: 0.1.0
|
native_imaging: 0.1.0
|
||||||
omemo_dart:
|
omemo_dart:
|
||||||
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
|
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
|
||||||
version: 0.4.3
|
version: 0.5.0
|
||||||
page_transition: 2.0.9
|
page_transition: 2.0.9
|
||||||
path: 1.8.2
|
path: 1.8.2
|
||||||
path_provider: 2.0.11
|
path_provider: 2.0.11
|
||||||
@ -139,8 +139,13 @@ dependency_overrides:
|
|||||||
moxxmpp:
|
moxxmpp:
|
||||||
git:
|
git:
|
||||||
url: https://codeberg.org/moxxy/moxxmpp.git
|
url: https://codeberg.org/moxxy/moxxmpp.git
|
||||||
rev: fa2ce7c2d10042246e19249f672ce32f90bab087
|
rev: 05e3d804a4036e9cd93fd27473a1e970fda3c3fc
|
||||||
path: packages/moxxmpp
|
path: packages/moxxmpp
|
||||||
|
|
||||||
|
omemo_dart:
|
||||||
|
git:
|
||||||
|
url: https://github.com/PapaTutuWawa/omemo_dart.git
|
||||||
|
rev: 49c7e114e6cf80dcde55fbbd218bba3182045862
|
||||||
|
|
||||||
extra_licenses:
|
extra_licenses:
|
||||||
- name: undraw.co
|
- name: undraw.co
|
||||||
|
Loading…
Reference in New Issue
Block a user