handle text and media server messages, update pushkeys

This commit is contained in:
otsmr 2025-10-19 14:44:49 +02:00
parent a4ccefec75
commit 2f3f927914
41 changed files with 1172 additions and 790 deletions

View file

@ -128,8 +128,8 @@ struct PushNotification: Sendable {
var kind: PushKind = .reaction
var messageID: Int64 {
get {return _messageID ?? 0}
var messageID: String {
get {return _messageID ?? String()}
set {_messageID = newValue}
}
/// Returns true if `messageID` has been explicitly set.
@ -150,7 +150,7 @@ struct PushNotification: Sendable {
init() {}
fileprivate var _messageID: Int64? = nil
fileprivate var _messageID: String? = nil
fileprivate var _reactionContent: String? = nil
}
@ -177,8 +177,8 @@ struct PushUser: Sendable {
var blocked: Bool = false
var lastMessageID: Int64 {
get {return _lastMessageID ?? 0}
var lastMessageID: String {
get {return _lastMessageID ?? String()}
set {_lastMessageID = newValue}
}
/// Returns true if `lastMessageID` has been explicitly set.
@ -192,7 +192,7 @@ struct PushUser: Sendable {
init() {}
fileprivate var _lastMessageID: Int64? = nil
fileprivate var _lastMessageID: String? = nil
}
struct PushKey: Sendable {
@ -273,7 +273,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularEnumField(value: &self.kind) }()
case 2: try { try decoder.decodeSingularInt64Field(value: &self._messageID) }()
case 2: try { try decoder.decodeSingularStringField(value: &self._messageID) }()
case 3: try { try decoder.decodeSingularStringField(value: &self._reactionContent) }()
default: break
}
@ -289,7 +289,7 @@ extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme
try visitor.visitSingularEnumField(value: self.kind, fieldNumber: 1)
}
try { if let v = self._messageID {
try visitor.visitSingularInt64Field(value: v, fieldNumber: 2)
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
} }()
try { if let v = self._reactionContent {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
@ -349,7 +349,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
case 1: try { try decoder.decodeSingularInt64Field(value: &self.userID) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.displayName) }()
case 3: try { try decoder.decodeSingularBoolField(value: &self.blocked) }()
case 4: try { try decoder.decodeSingularInt64Field(value: &self._lastMessageID) }()
case 4: try { try decoder.decodeSingularStringField(value: &self._lastMessageID) }()
case 5: try { try decoder.decodeRepeatedMessageField(value: &self.pushKeys) }()
default: break
}
@ -371,7 +371,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
try visitor.visitSingularBoolField(value: self.blocked, fieldNumber: 3)
}
try { if let v = self._lastMessageID {
try visitor.visitSingularInt64Field(value: v, fieldNumber: 4)
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
} }()
if !self.pushKeys.isEmpty {
try visitor.visitRepeatedMessageField(value: self.pushKeys, fieldNumber: 5)

View file

@ -6,6 +6,6 @@ class SecureStorageKeys {
static const String userData = 'userData';
static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
static const String receivingPushKeys = 'receiving_push_keys';
static const String sendingPushKeys = 'sending_push_keys';
static const String receivingPushKeys = 'push_keys_receiving';
static const String sendingPushKeys = 'push_keys_sending';
}

View file

@ -120,10 +120,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
Stream<List<Contact>> watchNotAcceptedContacts() {
return (select(contacts)
..where(
(t) =>
t.accepted.equals(false) &
t.archived.equals(false) &
t.blocked.equals(false),
(t) => t.accepted.equals(false) & t.blocked.equals(false),
))
.watch();
// return (select(contacts)).watch();

View file

@ -12,10 +12,18 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
GroupsDao(super.db);
Future<bool> isContactInGroup(int contactId, String groupId) async {
final entry = await (select(groupMembers)
..where(
final entry = await (select(groupMembers)..where(
// ignore: require_trailing_commas
(t) => t.contactId.equals(contactId) & t.groupId.equals(groupId)))
.getSingleOrNull();
return entry != null;
}
Future<void> updateGroup(
String groupId,
GroupsCompanion updates,
) async {
await (update(groups)..where((c) => c.groupId.equals(groupId)))
.write(updates);
}
}

View file

@ -0,0 +1,54 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'mediafiles.dao.g.dart';
@DriftAccessor(tables: [MediaFiles])
class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
with _$MediaFilesDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
// ignore: matching_super_parameters
MediaFilesDao(super.db);
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
try {
final rowId = await into(mediaFiles).insert(mediaFile);
return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId)))
.getSingle();
} catch (e) {
Log.error('Could not insert media file: $e');
return null;
}
}
Future<void> updateMedia(
String mediaId,
MediaFilesCompanion updates,
) async {
await (update(mediaFiles)..where((c) => c.mediaId.equals(mediaId)))
.write(updates);
}
Future<MediaFile?> getMediaFileById(String mediaId) async {
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
.getSingleOrNull();
}
Future<void> resetPendingDownloadState() async {
await (update(mediaFiles)
..where(
(c) => c.downloadState.equals(
DownloadState.downloading.name,
),
))
.write(
const MediaFilesCompanion(
downloadState: Value(DownloadState.pending),
),
);
}
}

View file

@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'mediafiles.dao.dart';
// ignore_for_file: type=lint
mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> {
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles;
}

View file

@ -1,13 +1,24 @@
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
part 'messages.dao.g.dart';
@DriftAccessor(tables: [Messages, Contacts, MediaFiles, MessageHistories])
@DriftAccessor(
tables: [
Messages,
Contacts,
MediaFiles,
MessageHistories,
Groups,
],
)
class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
@ -156,22 +167,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .write(updates);
// }
// Future<void> resetPendingDownloadState() {
// // All media files in the downloading state are reset to the pending state
// // When the app is used in mobile network, they will not be downloaded at the start
// // if they are not yet downloaded...
// const updates =
// MessagesCompanion(downloadState: Value(DownloadState.pending));
// return (update(messages)
// ..where(
// (t) =>
// t.messageOtherId.isNotNull() &
// t.downloadState.equals(DownloadState.downloading.index) &
// t.kind.equals(MessageKind.media.name),
// ))
// .write(updates);
// }
Future<void> handleMessageDeletion(
int contactId,
String messageId,
@ -278,28 +273,33 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// .write(updatedValues);
// }
// Future<void> updateMessageByMessageId(
// int messageId,
// MessagesCompanion updatedValues,
// ) {
// return (update(messages)..where((c) => c.messageId.equals(messageId)))
// .write(updatedValues);
// }
Future<void> updateMessageId(
String messageId,
MessagesCompanion updatedValues,
) {
return (update(messages)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues);
}
// Future<int?> insertMessage(MessagesCompanion message) async {
// try {
// await (update(contacts)
// ..where(
// (c) => c.userId.equals(message.contactId.value),
// ))
// .write(ContactsCompanion(lastMessageExchange: Value(DateTime.now())));
Future<Message?> insertMessage(MessagesCompanion message) async {
try {
final rowId = await into(messages).insert(message);
// return await into(messages).insert(message);
// } catch (e) {
// Log.error('Error while inserting message: $e');
// return null;
// }
// }
await twonlyDB.groupsDao.updateGroup(
message.groupId.value,
GroupsCompanion(
lastMessageExchange: Value(DateTime.now()),
archived: const Value(false),
),
);
return await (select(messages)..where((t) => t.rowId.equals(rowId)))
.getSingle();
} catch (e) {
Log.error('Could not insert message: $e');
return null;
}
}
// Future<void> deleteMessagesByContactId(int contactId) {
// return (delete(messages)

View file

@ -9,4 +9,5 @@ mixin _$MessagesDaoMixin on DatabaseAccessor<TwonlyDB> {
$MessagesTable get messages => attachedDatabase.messages;
$MessageHistoriesTable get messageHistories =>
attachedDatabase.messageHistories;
$GroupsTable get groups => attachedDatabase.groups;
}

View file

@ -31,11 +31,13 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
))
.go();
if (emoji != null) {
await into(reactions).insert(ReactionsCompanion(
await into(reactions).insert(
ReactionsCompanion(
messageId: Value(messageId),
emoji: Value(emoji),
senderId: Value(contactId),
));
),
);
}
} catch (e) {
Log.error(e);

View file

@ -15,8 +15,10 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> confirmReceipt(String receiptId, int fromUserId) async {
final receipt = await (select(receipts)
..where((t) =>
t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId)))
..where(
(t) =>
t.receiptId.equals(receiptId) & t.contactId.equals(fromUserId),
))
.getSingleOrNull();
if (receipt == null) return;
@ -26,7 +28,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
..where((t) => t.messageId.equals(receipt.messageId!)))
.write(
const MessagesCompanion(
acknowledgeByUser: Value(true),
ackByUser: Value(true),
),
);
}
@ -39,6 +41,14 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.go();
}
Future<void> deleteReceipt(String receiptId) async {
await (delete(receipts)
..where(
(t) => t.receiptId.equals(receiptId),
))
.go();
}
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
try {
final id = await into(receipts).insert(entry);
@ -49,4 +59,33 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
return null;
}
}
Future<Receipt?> getReceiptById(String receiptId) async {
try {
return await (select(receipts)
..where(
(t) => t.receiptId.equals(receiptId),
))
.getSingleOrNull();
} catch (e) {
Log.error(e);
return null;
}
}
Future<List<Receipt>> getReceiptsNotAckByServer() async {
return (select(receipts)
..where(
(t) => t.ackByServerAt.isNull(),
))
.get();
}
Future<void> updateReceipt(
String receiptId,
ReceiptsCompanion updates,
) async {
await (update(receipts)..where((c) => c.receiptId.equals(receiptId)))
.write(updates);
}
}

View file

@ -16,7 +16,6 @@ class Contacts extends Table {
BoolColumn get hidden => boolean().withDefault(const Constant(false))();
BoolColumn get blocked => boolean().withDefault(const Constant(false))();
BoolColumn get verified => boolean().withDefault(const Constant(false))();
BoolColumn get archived => boolean().withDefault(const Constant(false))();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
BoolColumn get alsoBestFriend =>

View file

@ -9,6 +9,7 @@ class Groups extends Table {
BoolColumn get isGroupAdmin => boolean()();
BoolColumn get isGroupOfTwo => boolean()();
BoolColumn get pinned => boolean().withDefault(const Constant(false))();
BoolColumn get archived => boolean().withDefault(const Constant(false))();
DateTimeColumn get lastMessageExchange =>
dateTime().withDefault(currentDateAndTime)();

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
@ -14,13 +16,11 @@ enum UploadState {
receiverNotified,
}
enum DownloadState {
pending,
}
enum DownloadState { pending, downloading }
@DataClassName('MediaFile')
class MediaFiles extends Table {
TextColumn get mediaId => text().clientDefault(() => uuid.v4())();
TextColumn get mediaId => text().clientDefault(() => uuid.v7())();
TextColumn get type => textEnum<MediaType>()();
@ -34,6 +34,9 @@ class MediaFiles extends Table {
BoolColumn get storedByContact =>
boolean().withDefault(const Constant(false))();
TextColumn get reuploadRequestedBy =>
text().map(IntListTypeConverter()).nullable()();
IntColumn get displayLimitInMilliseconds => integer().nullable()();
BlobColumn get downloadToken => blob().nullable()();
@ -46,3 +49,15 @@ class MediaFiles extends Table {
@override
Set<Column> get primaryKey => {mediaId};
}
class IntListTypeConverter extends TypeConverter<List<int>, String> {
@override
List<int> fromSql(String fromDb) {
return List<int>.from(jsonDecode(fromDb) as Iterable<dynamic>);
}
@override
String toSql(List<int> value) {
return json.encode(value);
}
}

View file

@ -1,11 +1,12 @@
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
@DataClassName('Message')
class Messages extends Table {
TextColumn get groupId => text()();
TextColumn get messageId => text()();
TextColumn get messageId => text().clientDefault(() => uuid.v7())();
// in case senderId is null, it was send by user itself
IntColumn get senderId =>
@ -23,10 +24,8 @@ class Messages extends Table {
BoolColumn get isEdited => boolean().withDefault(const Constant(false))();
BoolColumn get acknowledgeByUser =>
boolean().withDefault(const Constant(false))();
BoolColumn get acknowledgeByServer =>
boolean().withDefault(const Constant(false))();
BoolColumn get ackByUser => boolean().withDefault(const Constant(false))();
BoolColumn get ackByServer => boolean().withDefault(const Constant(false))();
IntColumn get openedByCounter => integer().withDefault(const Constant(0))();
DateTimeColumn get openedAt => dateTime().nullable()();

View file

@ -15,11 +15,14 @@ class Receipts extends Table {
.nullable()
.references(Messages, #messageId, onDelete: KeyAction.cascade)();
/// This is the protobuf 'Message'
BlobColumn get message => blob()();
BoolColumn get contactWillSendsReceipt =>
boolean().withDefault(const Constant(true))();
DateTimeColumn get ackByServerAt => dateTime().nullable()();
IntColumn get retryCount => integer().withDefault(const Constant(0))();
DateTimeColumn get lastRetry => dateTime().nullable()();

View file

@ -4,6 +4,7 @@ import 'package:drift_flutter/drift_flutter.dart'
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/daos/mediafiles.dao.dart';
import 'package:twonly/src/database/daos/messages.dao.dart';
import 'package:twonly/src/database/daos/reactions.dao.dart';
import 'package:twonly/src/database/daos/receipts.dao.dart';
@ -48,7 +49,8 @@ part 'twonly.db.g.dart';
SignalDao,
ReceiptsDao,
GroupsDao,
ReactionsDao
ReactionsDao,
MediaFilesDao,
],
)
class TwonlyDB extends _$TwonlyDB {

File diff suppressed because it is too large Load diff

View file

@ -218,6 +218,7 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage {
factory EncryptedContent_TextMessage({
$core.String? senderMessageId,
$core.String? text,
$fixnum.Int64? timestamp,
$core.String? quoteMessageId,
}) {
final $result = create();
@ -227,6 +228,9 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage {
if (text != null) {
$result.text = text;
}
if (timestamp != null) {
$result.timestamp = timestamp;
}
if (quoteMessageId != null) {
$result.quoteMessageId = quoteMessageId;
}
@ -239,7 +243,8 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.TextMessage', createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId')
..aOS(2, _omitFieldNames ? '' : 'text')
..aOS(3, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId')
..aInt64(3, _omitFieldNames ? '' : 'timestamp')
..aOS(4, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId')
..hasRequiredFields = false
;
@ -283,13 +288,22 @@ class EncryptedContent_TextMessage extends $pb.GeneratedMessage {
void clearText() => clearField(2);
@$pb.TagNumber(3)
$core.String get quoteMessageId => $_getSZ(2);
$fixnum.Int64 get timestamp => $_getI64(2);
@$pb.TagNumber(3)
set quoteMessageId($core.String v) { $_setString(2, v); }
set timestamp($fixnum.Int64 v) { $_setInt64(2, v); }
@$pb.TagNumber(3)
$core.bool hasQuoteMessageId() => $_has(2);
$core.bool hasTimestamp() => $_has(2);
@$pb.TagNumber(3)
void clearQuoteMessageId() => clearField(3);
void clearTimestamp() => clearField(3);
@$pb.TagNumber(4)
$core.String get quoteMessageId => $_getSZ(3);
@$pb.TagNumber(4)
set quoteMessageId($core.String v) { $_setString(3, v); }
@$pb.TagNumber(4)
$core.bool hasQuoteMessageId() => $_has(3);
@$pb.TagNumber(4)
void clearQuoteMessageId() => clearField(4);
}
class EncryptedContent_Reaction extends $pb.GeneratedMessage {
@ -374,6 +388,7 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
factory EncryptedContent_MessageUpdate({
EncryptedContent_MessageUpdate_Type? type,
$core.String? senderMessageId,
$core.Iterable<$core.String>? multipleSenderMessageIds,
$core.String? text,
$fixnum.Int64? timestamp,
}) {
@ -384,6 +399,9 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
if (senderMessageId != null) {
$result.senderMessageId = senderMessageId;
}
if (multipleSenderMessageIds != null) {
$result.multipleSenderMessageIds.addAll(multipleSenderMessageIds);
}
if (text != null) {
$result.text = text;
}
@ -399,8 +417,9 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.MessageUpdate', createEmptyInstance: create)
..e<EncryptedContent_MessageUpdate_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_MessageUpdate_Type.DELETE, valueOf: EncryptedContent_MessageUpdate_Type.valueOf, enumValues: EncryptedContent_MessageUpdate_Type.values)
..aOS(2, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId')
..aOS(3, _omitFieldNames ? '' : 'text')
..aInt64(4, _omitFieldNames ? '' : 'timestamp')
..pPS(3, _omitFieldNames ? '' : 'multipleSenderMessageIds', protoName: 'multipleSenderMessageIds')
..aOS(4, _omitFieldNames ? '' : 'text')
..aInt64(5, _omitFieldNames ? '' : 'timestamp')
..hasRequiredFields = false
;
@ -444,22 +463,25 @@ class EncryptedContent_MessageUpdate extends $pb.GeneratedMessage {
void clearSenderMessageId() => clearField(2);
@$pb.TagNumber(3)
$core.String get text => $_getSZ(2);
@$pb.TagNumber(3)
set text($core.String v) { $_setString(2, v); }
@$pb.TagNumber(3)
$core.bool hasText() => $_has(2);
@$pb.TagNumber(3)
void clearText() => clearField(3);
$core.List<$core.String> get multipleSenderMessageIds => $_getList(2);
@$pb.TagNumber(4)
$fixnum.Int64 get timestamp => $_getI64(3);
$core.String get text => $_getSZ(3);
@$pb.TagNumber(4)
set timestamp($fixnum.Int64 v) { $_setInt64(3, v); }
set text($core.String v) { $_setString(3, v); }
@$pb.TagNumber(4)
$core.bool hasTimestamp() => $_has(3);
$core.bool hasText() => $_has(3);
@$pb.TagNumber(4)
void clearTimestamp() => clearField(4);
void clearText() => clearField(4);
@$pb.TagNumber(5)
$fixnum.Int64 get timestamp => $_getI64(4);
@$pb.TagNumber(5)
set timestamp($fixnum.Int64 v) { $_setInt64(4, v); }
@$pb.TagNumber(5)
$core.bool hasTimestamp() => $_has(4);
@$pb.TagNumber(5)
void clearTimestamp() => clearField(5);
}
class EncryptedContent_Media extends $pb.GeneratedMessage {
@ -468,6 +490,8 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
EncryptedContent_Media_Type? type,
$fixnum.Int64? displayLimitInMilliseconds,
$core.bool? requiresAuthentication,
$fixnum.Int64? timestamp,
$core.String? quoteMessageId,
$core.List<$core.int>? downloadToken,
$core.List<$core.int>? encryptionKey,
$core.List<$core.int>? encryptionMac,
@ -486,6 +510,12 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
if (requiresAuthentication != null) {
$result.requiresAuthentication = requiresAuthentication;
}
if (timestamp != null) {
$result.timestamp = timestamp;
}
if (quoteMessageId != null) {
$result.quoteMessageId = quoteMessageId;
}
if (downloadToken != null) {
$result.downloadToken = downloadToken;
}
@ -506,13 +536,15 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EncryptedContent.Media', createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'senderMessageId', protoName: 'senderMessageId')
..e<EncryptedContent_Media_Type>(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_Media_Type.IMAGE, valueOf: EncryptedContent_Media_Type.valueOf, enumValues: EncryptedContent_Media_Type.values)
..e<EncryptedContent_Media_Type>(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_Media_Type.REUPLOAD, valueOf: EncryptedContent_Media_Type.valueOf, enumValues: EncryptedContent_Media_Type.values)
..aInt64(3, _omitFieldNames ? '' : 'displayLimitInMilliseconds', protoName: 'displayLimitInMilliseconds')
..aOB(4, _omitFieldNames ? '' : 'requiresAuthentication', protoName: 'requiresAuthentication')
..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'downloadToken', $pb.PbFieldType.OY, protoName: 'downloadToken')
..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'encryptionKey', $pb.PbFieldType.OY, protoName: 'encryptionKey')
..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'encryptionMac', $pb.PbFieldType.OY, protoName: 'encryptionMac')
..a<$core.List<$core.int>>(8, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY, protoName: 'encryptionNonce')
..aInt64(5, _omitFieldNames ? '' : 'timestamp')
..aOS(6, _omitFieldNames ? '' : 'quoteMessageId', protoName: 'quoteMessageId')
..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'downloadToken', $pb.PbFieldType.OY, protoName: 'downloadToken')
..a<$core.List<$core.int>>(8, _omitFieldNames ? '' : 'encryptionKey', $pb.PbFieldType.OY, protoName: 'encryptionKey')
..a<$core.List<$core.int>>(9, _omitFieldNames ? '' : 'encryptionMac', $pb.PbFieldType.OY, protoName: 'encryptionMac')
..a<$core.List<$core.int>>(10, _omitFieldNames ? '' : 'encryptionNonce', $pb.PbFieldType.OY, protoName: 'encryptionNonce')
..hasRequiredFields = false
;
@ -574,40 +606,58 @@ class EncryptedContent_Media extends $pb.GeneratedMessage {
void clearRequiresAuthentication() => clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get downloadToken => $_getN(4);
$fixnum.Int64 get timestamp => $_getI64(4);
@$pb.TagNumber(5)
set downloadToken($core.List<$core.int> v) { $_setBytes(4, v); }
set timestamp($fixnum.Int64 v) { $_setInt64(4, v); }
@$pb.TagNumber(5)
$core.bool hasDownloadToken() => $_has(4);
$core.bool hasTimestamp() => $_has(4);
@$pb.TagNumber(5)
void clearDownloadToken() => clearField(5);
void clearTimestamp() => clearField(5);
@$pb.TagNumber(6)
$core.List<$core.int> get encryptionKey => $_getN(5);
$core.String get quoteMessageId => $_getSZ(5);
@$pb.TagNumber(6)
set encryptionKey($core.List<$core.int> v) { $_setBytes(5, v); }
set quoteMessageId($core.String v) { $_setString(5, v); }
@$pb.TagNumber(6)
$core.bool hasEncryptionKey() => $_has(5);
$core.bool hasQuoteMessageId() => $_has(5);
@$pb.TagNumber(6)
void clearEncryptionKey() => clearField(6);
void clearQuoteMessageId() => clearField(6);
@$pb.TagNumber(7)
$core.List<$core.int> get encryptionMac => $_getN(6);
$core.List<$core.int> get downloadToken => $_getN(6);
@$pb.TagNumber(7)
set encryptionMac($core.List<$core.int> v) { $_setBytes(6, v); }
set downloadToken($core.List<$core.int> v) { $_setBytes(6, v); }
@$pb.TagNumber(7)
$core.bool hasEncryptionMac() => $_has(6);
$core.bool hasDownloadToken() => $_has(6);
@$pb.TagNumber(7)
void clearEncryptionMac() => clearField(7);
void clearDownloadToken() => clearField(7);
@$pb.TagNumber(8)
$core.List<$core.int> get encryptionNonce => $_getN(7);
$core.List<$core.int> get encryptionKey => $_getN(7);
@$pb.TagNumber(8)
set encryptionNonce($core.List<$core.int> v) { $_setBytes(7, v); }
set encryptionKey($core.List<$core.int> v) { $_setBytes(7, v); }
@$pb.TagNumber(8)
$core.bool hasEncryptionNonce() => $_has(7);
$core.bool hasEncryptionKey() => $_has(7);
@$pb.TagNumber(8)
void clearEncryptionNonce() => clearField(8);
void clearEncryptionKey() => clearField(8);
@$pb.TagNumber(9)
$core.List<$core.int> get encryptionMac => $_getN(8);
@$pb.TagNumber(9)
set encryptionMac($core.List<$core.int> v) { $_setBytes(8, v); }
@$pb.TagNumber(9)
$core.bool hasEncryptionMac() => $_has(8);
@$pb.TagNumber(9)
void clearEncryptionMac() => clearField(9);
@$pb.TagNumber(10)
$core.List<$core.int> get encryptionNonce => $_getN(9);
@$pb.TagNumber(10)
set encryptionNonce($core.List<$core.int> v) { $_setBytes(9, v); }
@$pb.TagNumber(10)
$core.bool hasEncryptionNonce() => $_has(9);
@$pb.TagNumber(10)
void clearEncryptionNonce() => clearField(10);
}
class EncryptedContent_MediaUpdate extends $pb.GeneratedMessage {
@ -807,6 +857,7 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage {
EncryptedContent_PushKeys_Type? type,
$fixnum.Int64? keyId,
$core.List<$core.int>? key,
$fixnum.Int64? createdAt,
}) {
final $result = create();
if (type != null) {
@ -818,6 +869,9 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage {
if (key != null) {
$result.key = key;
}
if (createdAt != null) {
$result.createdAt = createdAt;
}
return $result;
}
EncryptedContent_PushKeys._() : super();
@ -828,6 +882,7 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage {
..e<EncryptedContent_PushKeys_Type>(1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: EncryptedContent_PushKeys_Type.REQUEST, valueOf: EncryptedContent_PushKeys_Type.valueOf, enumValues: EncryptedContent_PushKeys_Type.values)
..aInt64(2, _omitFieldNames ? '' : 'keyId', protoName: 'keyId')
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'key', $pb.PbFieldType.OY)
..aInt64(4, _omitFieldNames ? '' : 'createdAt', protoName: 'createdAt')
..hasRequiredFields = false
;
@ -878,6 +933,15 @@ class EncryptedContent_PushKeys extends $pb.GeneratedMessage {
$core.bool hasKey() => $_has(2);
@$pb.TagNumber(3)
void clearKey() => clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get createdAt => $_getI64(3);
@$pb.TagNumber(4)
set createdAt($fixnum.Int64 v) { $_setInt64(3, v); }
@$pb.TagNumber(4)
$core.bool hasCreatedAt() => $_has(3);
@$pb.TagNumber(4)
void clearCreatedAt() => clearField(4);
}
class EncryptedContent_FlameSync extends $pb.GeneratedMessage {

View file

@ -18,12 +18,14 @@ class Message_Type extends $pb.ProtobufEnum {
static const Message_Type PLAINTEXT_CONTENT = Message_Type._(1, _omitEnumNames ? '' : 'PLAINTEXT_CONTENT');
static const Message_Type CIPHERTEXT = Message_Type._(2, _omitEnumNames ? '' : 'CIPHERTEXT');
static const Message_Type PREKEY_BUNDLE = Message_Type._(3, _omitEnumNames ? '' : 'PREKEY_BUNDLE');
static const Message_Type TEST_NOTIFICATION = Message_Type._(4, _omitEnumNames ? '' : 'TEST_NOTIFICATION');
static const $core.List<Message_Type> values = <Message_Type> [
SENDER_DELIVERY_RECEIPT,
PLAINTEXT_CONTENT,
CIPHERTEXT,
PREKEY_BUNDLE,
TEST_NOTIFICATION,
];
static final $core.Map<$core.int, Message_Type> _byValue = $pb.ProtobufEnum.initByValue(values);
@ -65,11 +67,13 @@ class EncryptedContent_MessageUpdate_Type extends $pb.ProtobufEnum {
}
class EncryptedContent_Media_Type extends $pb.ProtobufEnum {
static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(0, _omitEnumNames ? '' : 'IMAGE');
static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'VIDEO');
static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'GIF');
static const EncryptedContent_Media_Type REUPLOAD = EncryptedContent_Media_Type._(0, _omitEnumNames ? '' : 'REUPLOAD');
static const EncryptedContent_Media_Type IMAGE = EncryptedContent_Media_Type._(1, _omitEnumNames ? '' : 'IMAGE');
static const EncryptedContent_Media_Type VIDEO = EncryptedContent_Media_Type._(2, _omitEnumNames ? '' : 'VIDEO');
static const EncryptedContent_Media_Type GIF = EncryptedContent_Media_Type._(3, _omitEnumNames ? '' : 'GIF');
static const $core.List<EncryptedContent_Media_Type> values = <EncryptedContent_Media_Type> [
REUPLOAD,
IMAGE,
VIDEO,
GIF,

View file

@ -37,6 +37,7 @@ const Message_Type$json = {
{'1': 'PLAINTEXT_CONTENT', '2': 1},
{'1': 'CIPHERTEXT', '2': 2},
{'1': 'PREKEY_BUNDLE', '2': 3},
{'1': 'TEST_NOTIFICATION', '2': 4},
],
};
@ -45,9 +46,10 @@ final $typed_data.Uint8List messageDescriptor = $convert.base64Decode(
'CgdNZXNzYWdlEiEKBHR5cGUYASABKA4yDS5NZXNzYWdlLlR5cGVSBHR5cGUSHAoJcmVjZWlwdE'
'lkGAIgASgJUglyZWNlaXB0SWQSLwoQZW5jcnlwdGVkQ29udGVudBgDIAEoDEgAUhBlbmNyeXB0'
'ZWRDb250ZW50iAEBEkIKEHBsYWludGV4dENvbnRlbnQYBCABKAsyES5QbGFpbnRleHRDb250ZW'
'50SAFSEHBsYWludGV4dENvbnRlbnSIAQEiXQoEVHlwZRIbChdTRU5ERVJfREVMSVZFUllfUkVD'
'50SAFSEHBsYWludGV4dENvbnRlbnSIAQEidAoEVHlwZRIbChdTRU5ERVJfREVMSVZFUllfUkVD'
'RUlQVBAAEhUKEVBMQUlOVEVYVF9DT05URU5UEAESDgoKQ0lQSEVSVEVYVBACEhEKDVBSRUtFWV'
'9CVU5ETEUQA0ITChFfZW5jcnlwdGVkQ29udGVudEITChFfcGxhaW50ZXh0Q29udGVudA==');
'9CVU5ETEUQAxIVChFURVNUX05PVElGSUNBVElPThAEQhMKEV9lbmNyeXB0ZWRDb250ZW50QhMK'
'EV9wbGFpbnRleHRDb250ZW50');
@$core.Deprecated('Use plaintextContentDescriptor instead')
const PlaintextContent$json = {
@ -126,7 +128,8 @@ const EncryptedContent_TextMessage$json = {
'2': [
{'1': 'senderMessageId', '3': 1, '4': 1, '5': 9, '10': 'senderMessageId'},
{'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'},
{'1': 'quoteMessageId', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'quoteMessageId', '17': true},
{'1': 'timestamp', '3': 3, '4': 1, '5': 3, '10': 'timestamp'},
{'1': 'quoteMessageId', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'quoteMessageId', '17': true},
],
'8': [
{'1': '_quoteMessageId'},
@ -152,14 +155,15 @@ const EncryptedContent_MessageUpdate$json = {
'1': 'MessageUpdate',
'2': [
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.MessageUpdate.Type', '10': 'type'},
{'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '10': 'senderMessageId'},
{'1': 'text', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'text', '17': true},
{'1': 'timestamp', '3': 4, '4': 1, '5': 3, '9': 1, '10': 'timestamp', '17': true},
{'1': 'senderMessageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'senderMessageId', '17': true},
{'1': 'multipleSenderMessageIds', '3': 3, '4': 3, '5': 9, '10': 'multipleSenderMessageIds'},
{'1': 'text', '3': 4, '4': 1, '5': 9, '9': 1, '10': 'text', '17': true},
{'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'},
],
'4': [EncryptedContent_MessageUpdate_Type$json],
'8': [
{'1': '_senderMessageId'},
{'1': '_text'},
{'1': '_timestamp'},
],
};
@ -181,14 +185,17 @@ const EncryptedContent_Media$json = {
{'1': 'type', '3': 2, '4': 1, '5': 14, '6': '.EncryptedContent.Media.Type', '10': 'type'},
{'1': 'displayLimitInMilliseconds', '3': 3, '4': 1, '5': 3, '9': 0, '10': 'displayLimitInMilliseconds', '17': true},
{'1': 'requiresAuthentication', '3': 4, '4': 1, '5': 8, '10': 'requiresAuthentication'},
{'1': 'downloadToken', '3': 5, '4': 1, '5': 12, '9': 1, '10': 'downloadToken', '17': true},
{'1': 'encryptionKey', '3': 6, '4': 1, '5': 12, '9': 2, '10': 'encryptionKey', '17': true},
{'1': 'encryptionMac', '3': 7, '4': 1, '5': 12, '9': 3, '10': 'encryptionMac', '17': true},
{'1': 'encryptionNonce', '3': 8, '4': 1, '5': 12, '9': 4, '10': 'encryptionNonce', '17': true},
{'1': 'timestamp', '3': 5, '4': 1, '5': 3, '10': 'timestamp'},
{'1': 'quoteMessageId', '3': 6, '4': 1, '5': 9, '9': 1, '10': 'quoteMessageId', '17': true},
{'1': 'downloadToken', '3': 7, '4': 1, '5': 12, '9': 2, '10': 'downloadToken', '17': true},
{'1': 'encryptionKey', '3': 8, '4': 1, '5': 12, '9': 3, '10': 'encryptionKey', '17': true},
{'1': 'encryptionMac', '3': 9, '4': 1, '5': 12, '9': 4, '10': 'encryptionMac', '17': true},
{'1': 'encryptionNonce', '3': 10, '4': 1, '5': 12, '9': 5, '10': 'encryptionNonce', '17': true},
],
'4': [EncryptedContent_Media_Type$json],
'8': [
{'1': '_displayLimitInMilliseconds'},
{'1': '_quoteMessageId'},
{'1': '_downloadToken'},
{'1': '_encryptionKey'},
{'1': '_encryptionMac'},
@ -200,9 +207,10 @@ const EncryptedContent_Media$json = {
const EncryptedContent_Media_Type$json = {
'1': 'Type',
'2': [
{'1': 'IMAGE', '2': 0},
{'1': 'VIDEO', '2': 1},
{'1': 'GIF', '2': 2},
{'1': 'REUPLOAD', '2': 0},
{'1': 'IMAGE', '2': 1},
{'1': 'VIDEO', '2': 2},
{'1': 'GIF', '2': 3},
],
};
@ -276,11 +284,13 @@ const EncryptedContent_PushKeys$json = {
{'1': 'type', '3': 1, '4': 1, '5': 14, '6': '.EncryptedContent.PushKeys.Type', '10': 'type'},
{'1': 'keyId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'keyId', '17': true},
{'1': 'key', '3': 3, '4': 1, '5': 12, '9': 1, '10': 'key', '17': true},
{'1': 'createdAt', '3': 4, '4': 1, '5': 3, '9': 2, '10': 'createdAt', '17': true},
],
'4': [EncryptedContent_PushKeys_Type$json],
'8': [
{'1': '_keyId'},
{'1': '_key'},
{'1': '_createdAt'},
],
};
@ -317,42 +327,47 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'N0UmVxdWVzdEgHUg5jb250YWN0UmVxdWVzdIgBARI+CglmbGFtZVN5bmMYCiABKAsyGy5FbmNy'
'eXB0ZWRDb250ZW50LkZsYW1lU3luY0gIUglmbGFtZVN5bmOIAQESOwoIcHVzaEtleXMYCyABKA'
'syGi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSAlSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9u'
'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqLAQoLVG'
'GAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgKUghyZWFjdGlvbogBARqpAQoLVG'
'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE'
'dGV4dBgCIAEoCVIEdGV4dBIrCg5xdW90ZU1lc3NhZ2VJZBgDIAEoCUgAUg5xdW90ZU1lc3NhZ2'
'VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQagQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJ'
'ZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEhkKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBn'
'JlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFCCAoGX2Vtb2ppQgkKB19yZW1vdmUa9QEKDU1lc3Nh'
'Z2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS'
'5UeXBlUgR0eXBlEigKD3NlbmRlck1lc3NhZ2VJZBgCIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhcK'
'BHRleHQYAyABKAlIAFIEdGV4dIgBARIhCgl0aW1lc3RhbXAYBCABKANIAVIJdGltZXN0YW1wiA'
'EBIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCBwoFX3Rl'
'eHRCDAoKX3RpbWVzdGFtcBqgBAoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW'
'5kZXJNZXNzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlw'
'ZVIEdHlwZRJDChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TG'
'ltaXRJbk1pbGxpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZy'
'ZXF1aXJlc0F1dGhlbnRpY2F0aW9uEikKDWRvd25sb2FkVG9rZW4YBSABKAxIAVINZG93bmxvYW'
'RUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAYgASgMSAJSDWVuY3J5cHRpb25LZXmIAQESKQoN'
'ZW5jcnlwdGlvbk1hYxgHIAEoDEgDUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRpb25Ob2'
'5jZRgIIAEoDEgEUg9lbmNyeXB0aW9uTm9uY2WIAQEiJQoEVHlwZRIJCgVJTUFHRRAAEgkKBVZJ'
'REVPEAESBwoDR0lGEAJCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhAKDl9kb3dubG'
'9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0'
'aW9uTm9uY2UapwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW'
'50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJn'
'ZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUF'
'RJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVk'
'Q29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEg'
'oKBlJFSkVDVBABEgoKBkFDQ0VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4y'
'JC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdm'
'cYAiABKAlIAFIJYXZhdGFyU3ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlO'
'YW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDg'
'oMX2Rpc3BsYXlOYW1lGqQBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29u'
'dGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2'
'tleRgDIAEoDEgBUgNrZXmIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoG'
'X2tleUlkQgYKBF9rZXkahwEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW'
'1lQ291bnRlchI2ChZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3Vu'
'dGVyQ2hhbmdlEh4KCmJlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmRCCgoIX2dyb3VwSWRCFw'
'oVX3NlbmRlclByb2ZpbGVDb3VudGVyQg4KDF90ZXh0TWVzc2FnZUIQCg5fbWVzc2FnZVVwZGF0'
'ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YW'
'N0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb24=');
'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU'
'1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa'
'gQEKCFJlYWN0aW9uEigKD3RhcmdldE1lc3NhZ2VJZBgBIAEoCVIPdGFyZ2V0TWVzc2FnZUlkEh'
'kKBWVtb2ppGAIgASgJSABSBWVtb2ppiAEBEhsKBnJlbW92ZRgDIAEoCEgBUgZyZW1vdmWIAQFC'
'CAoGX2Vtb2ppQgkKB19yZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk'
'VuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3Nh'
'Z2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVTZW5kZXJNZXNzYW'
'dlSWRzGAMgAygJUhhtdWx0aXBsZVNlbmRlck1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0'
'ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEA'
'ASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4'
'dBqMBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMA'
'oEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNw'
'bGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb2'
'5kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRp'
'Y2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGA'
'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93'
'bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQ'
'ESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRp'
'b25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiMwoEVHlwZRIMCghSRVVQTE9BRB'
'AAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQA0IdChtfZGlzcGxheUxpbWl0SW5NaWxs'
'aXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeX'
'B0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlh'
'VXBkYXRlEjYKBHR5cGUYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cG'
'VSBHR5cGUSKAoPdGFyZ2V0TWVzc2FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlw'
'ZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db2'
'50YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVx'
'dWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0'
'VQVBACGtIBCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50'
'LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRIhCglhdmF0YXJTdmcYAiABKAlIAFIJYXZhdGFyU3'
'ZniAEBEiUKC2Rpc3BsYXlOYW1lGAMgASgJSAFSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoH'
'UkVRVUVTVBAAEgoKBlVQREFURRABQgwKCl9hdmF0YXJTdmdCDgoMX2Rpc3BsYXlOYW1lGtUBCg'
'hQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBl'
'UgR0eXBlEhkKBWtleUlkGAIgASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQ'
'ESIQoJY3JlYXRlZEF0GAQgASgDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQ'
'ABIKCgZVUERBVEUQAUIICgZfa2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GocBCglGbGFtZV'
'N5bmMSIgoMZmxhbWVDb3VudGVyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291'
'bnRlckNoYW5nZRgCIAEoA1IWbGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGA'
'MgASgIUgpiZXN0RnJpZW5kQgoKCF9ncm91cElkQhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIO'
'CgxfdGV4dE1lc3NhZ2VCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZG'
'F0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0IL'
'CglfcHVzaEtleXNCCwoJX3JlYWN0aW9u');

View file

@ -113,7 +113,7 @@ class EncryptedPushNotification extends $pb.GeneratedMessage {
class PushNotification extends $pb.GeneratedMessage {
factory PushNotification({
PushKind? kind,
$fixnum.Int64? messageId,
$core.String? messageId,
$core.String? reactionContent,
}) {
final $result = create();
@ -134,7 +134,7 @@ class PushNotification extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PushNotification', createEmptyInstance: create)
..e<PushKind>(1, _omitFieldNames ? '' : 'kind', $pb.PbFieldType.OE, defaultOrMaker: PushKind.reaction, valueOf: PushKind.valueOf, enumValues: PushKind.values)
..aInt64(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId')
..aOS(2, _omitFieldNames ? '' : 'messageId', protoName: 'messageId')
..aOS(3, _omitFieldNames ? '' : 'reactionContent', protoName: 'reactionContent')
..hasRequiredFields = false
;
@ -170,9 +170,9 @@ class PushNotification extends $pb.GeneratedMessage {
void clearKind() => clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get messageId => $_getI64(1);
$core.String get messageId => $_getSZ(1);
@$pb.TagNumber(2)
set messageId($fixnum.Int64 v) { $_setInt64(1, v); }
set messageId($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasMessageId() => $_has(1);
@$pb.TagNumber(2)
@ -237,7 +237,7 @@ class PushUser extends $pb.GeneratedMessage {
$fixnum.Int64? userId,
$core.String? displayName,
$core.bool? blocked,
$fixnum.Int64? lastMessageId,
$core.String? lastMessageId,
$core.Iterable<PushKey>? pushKeys,
}) {
final $result = create();
@ -266,7 +266,7 @@ class PushUser extends $pb.GeneratedMessage {
..aInt64(1, _omitFieldNames ? '' : 'userId', protoName: 'userId')
..aOS(2, _omitFieldNames ? '' : 'displayName', protoName: 'displayName')
..aOB(3, _omitFieldNames ? '' : 'blocked')
..aInt64(4, _omitFieldNames ? '' : 'lastMessageId', protoName: 'lastMessageId')
..aOS(4, _omitFieldNames ? '' : 'lastMessageId', protoName: 'lastMessageId')
..pc<PushKey>(5, _omitFieldNames ? '' : 'pushKeys', $pb.PbFieldType.PM, protoName: 'pushKeys', subBuilder: PushKey.create)
..hasRequiredFields = false
;
@ -320,9 +320,9 @@ class PushUser extends $pb.GeneratedMessage {
void clearBlocked() => clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get lastMessageId => $_getI64(3);
$core.String get lastMessageId => $_getSZ(3);
@$pb.TagNumber(4)
set lastMessageId($fixnum.Int64 v) { $_setInt64(3, v); }
set lastMessageId($core.String v) { $_setString(3, v); }
@$pb.TagNumber(4)
$core.bool hasLastMessageId() => $_has(3);
@$pb.TagNumber(4)

View file

@ -64,7 +64,7 @@ const PushNotification$json = {
'1': 'PushNotification',
'2': [
{'1': 'kind', '3': 1, '4': 1, '5': 14, '6': '.PushKind', '10': 'kind'},
{'1': 'messageId', '3': 2, '4': 1, '5': 3, '9': 0, '10': 'messageId', '17': true},
{'1': 'messageId', '3': 2, '4': 1, '5': 9, '9': 0, '10': 'messageId', '17': true},
{'1': 'reactionContent', '3': 3, '4': 1, '5': 9, '9': 1, '10': 'reactionContent', '17': true},
],
'8': [
@ -76,7 +76,7 @@ const PushNotification$json = {
/// Descriptor for `PushNotification`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List pushNotificationDescriptor = $convert.base64Decode(
'ChBQdXNoTm90aWZpY2F0aW9uEh0KBGtpbmQYASABKA4yCS5QdXNoS2luZFIEa2luZBIhCgltZX'
'NzYWdlSWQYAiABKANIAFIJbWVzc2FnZUlkiAEBEi0KD3JlYWN0aW9uQ29udGVudBgDIAEoCUgB'
'NzYWdlSWQYAiABKAlIAFIJbWVzc2FnZUlkiAEBEi0KD3JlYWN0aW9uQ29udGVudBgDIAEoCUgB'
'Ug9yZWFjdGlvbkNvbnRlbnSIAQFCDAoKX21lc3NhZ2VJZEISChBfcmVhY3Rpb25Db250ZW50');
@$core.Deprecated('Use pushUsersDescriptor instead')
@ -98,7 +98,7 @@ const PushUser$json = {
{'1': 'userId', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{'1': 'displayName', '3': 2, '4': 1, '5': 9, '10': 'displayName'},
{'1': 'blocked', '3': 3, '4': 1, '5': 8, '10': 'blocked'},
{'1': 'lastMessageId', '3': 4, '4': 1, '5': 3, '9': 0, '10': 'lastMessageId', '17': true},
{'1': 'lastMessageId', '3': 4, '4': 1, '5': 9, '9': 0, '10': 'lastMessageId', '17': true},
{'1': 'pushKeys', '3': 5, '4': 3, '5': 11, '6': '.PushKey', '10': 'pushKeys'},
],
'8': [
@ -110,7 +110,7 @@ const PushUser$json = {
final $typed_data.Uint8List pushUserDescriptor = $convert.base64Decode(
'CghQdXNoVXNlchIWCgZ1c2VySWQYASABKANSBnVzZXJJZBIgCgtkaXNwbGF5TmFtZRgCIAEoCV'
'ILZGlzcGxheU5hbWUSGAoHYmxvY2tlZBgDIAEoCFIHYmxvY2tlZBIpCg1sYXN0TWVzc2FnZUlk'
'GAQgASgDSABSDWxhc3RNZXNzYWdlSWSIAQESJAoIcHVzaEtleXMYBSADKAsyCC5QdXNoS2V5Ug'
'GAQgASgJSABSDWxhc3RNZXNzYWdlSWSIAQESJAoIcHVzaEtleXMYBSADKAsyCC5QdXNoS2V5Ug'
'hwdXNoS2V5c0IQCg5fbGFzdE1lc3NhZ2VJZA==');
@$core.Deprecated('Use pushKeyDescriptor instead')

View file

@ -6,6 +6,7 @@ message Message {
PLAINTEXT_CONTENT = 1;
CIPHERTEXT = 2;
PREKEY_BUNDLE = 3;
TEST_NOTIFICATION = 4;
}
Type type = 1;
string receiptId = 2;
@ -46,7 +47,8 @@ message EncryptedContent {
message TextMessage {
string senderMessageId = 1;
string text = 2;
optional string quoteMessageId = 3;
int64 timestamp = 3;
optional string quoteMessageId = 4;
}
message Reaction {
@ -62,27 +64,31 @@ message EncryptedContent {
OPENED = 2;
}
Type type = 1;
string senderMessageId = 2;
optional string text = 3;
optional int64 timestamp = 4;
optional string senderMessageId = 2;
repeated string multipleSenderMessageIds = 3;
optional string text = 4;
int64 timestamp = 5;
}
message Media {
enum Type {
IMAGE = 0;
VIDEO = 1;
GIF = 2;
REUPLOAD = 0;
IMAGE = 1;
VIDEO = 2;
GIF = 3;
}
string senderMessageId = 1;
Type type = 2;
optional int64 displayLimitInMilliseconds = 3;
bool requiresAuthentication = 4;
int64 timestamp = 5;
optional string quoteMessageId = 6;
optional bytes downloadToken = 5;
optional bytes encryptionKey = 6;
optional bytes encryptionMac = 7;
optional bytes encryptionNonce = 8;
optional bytes downloadToken = 7;
optional bytes encryptionKey = 8;
optional bytes encryptionMac = 9;
optional bytes encryptionNonce = 10;
}
message MediaUpdate {
@ -124,6 +130,7 @@ message EncryptedContent {
Type type = 1;
optional int64 keyId = 2;
optional bytes key = 3;
optional int64 createdAt = 4;
}
message FlameSync {

View file

@ -26,7 +26,7 @@ enum PushKind {
message PushNotification {
PushKind kind = 1;
optional int64 messageId = 2;
optional string messageId = 2;
optional string reactionContent = 3;
}
@ -39,7 +39,7 @@ message PushUser {
int64 userId = 1;
string displayName = 2;
bool blocked = 3;
optional int64 lastMessageId = 4;
optional string lastMessageId = 4;
repeated PushKey pushKeys = 5;
}

View file

@ -114,7 +114,7 @@ class ApiService {
_channel = null;
isAuthenticated = false;
globalCallbackConnectionState(isConnected: false);
await twonlyDB.messagesDao.resetPendingDownloadState();
await twonlyDB.mediaFilesDao.resetPendingDownloadState();
}
Future<void> startReconnectionTimer() async {

View file

@ -119,7 +119,7 @@ Future<void> handleDownloadStatusUpdateInternal(
Mutex protectDownload = Mutex();
Future<void> startDownloadMedia(Message message, bool force) async {
Future<void> startDownloadMedia(MediaFile media, bool force) async {
Log.info(
'Download blocked for ${message.messageId} because of network state.',
);

View file

@ -1,163 +1,153 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/services/api/server_messages.dart'
show messageGetsAck;
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
final lockRetransmission = Mutex();
Future<void> tryTransmitMessages() async {
return lockRetransmission.protect(() async {
final retransIds =
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
final receipts = await twonlyDB.receiptsDao.getReceiptsNotAckByServer();
Log.info('Retransmitting ${retransIds.length} text messages');
if (receipts.isEmpty) return;
if (retransIds.isEmpty) return;
Log.info('Reuploading ${receipts.length} messages to the server.');
for (final retransId in retransIds) {
await sendRetransmitMessage(retransId);
for (final receipt in receipts) {
await tryToSendCompleteMessage(receipt: receipt);
}
});
}
Future<void> tryToSendCompleteMessage(String receiptId) async {
Future<void> tryToSendCompleteMessage({
String? receiptId,
Receipt? receipt,
bool reupload = false,
}) async {
try {
final retrans = await twonlyDB.messageRetransmissionDao
.getRetransmissionById(retransId)
.getSingleOrNull();
/// SET THE Message().receiptID !!!!!!!
/// ALSO THE encryptedContent is NOT YET ENCRYPTED!
if (retrans == null) {
Log.error('$retransId not found in database');
if (receiptId == null && receipt == null) return;
if (receipt == null) {
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) {
Log.error('Receipt $receiptId not found.');
return;
}
if (retrans.acknowledgeByServerAt != null) {
Log.error('$retransId message already retransmitted');
return;
}
receiptId = receipt.receiptId;
final json = MessageJson.fromJson(
jsonDecode(
utf8.decode(
gzip.decode(retrans.plaintextContent),
if (reupload) {
await twonlyDB.receiptsDao.updateReceipt(
receiptId,
const ReceiptsCompanion(
ackByServerAt: Value(null),
),
) as Map<String, dynamic>,
);
Log.info('Retransmitting $retransId: ${json.kind} to ${retrans.contactId}');
final contact = await twonlyDB.contactsDao
.getContactByUserId(retrans.contactId)
.getSingleOrNull();
if (contact == null || contact.deleted) {
Log.warn('Contact deleted $retransId or not found in database.');
await twonlyDB.messageRetransmissionDao
.deleteRetransmissionById(retransId);
if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!,
const MessagesCompanion(errorWhileSending: Value(true)),
);
}
if (receipt.ackByServerAt != null) {
Log.error('$receiptId message already uploaded!');
return;
}
final encryptedBytes = await signalEncryptMessage(
retrans.contactId,
retrans.plaintextContent,
Log.info('Uploading $receiptId (Message to ${receipt.contactId})');
final message = pb.Message.fromBuffer(receipt.message)
..receiptId = receiptId;
final encryptedContent =
pb.EncryptedContent.fromBuffer(message.encryptedContent);
var pushData = await getPushDataFromEncryptedContent(
receipt.contactId,
receipt.messageId,
encryptedContent,
);
if (encryptedBytes == null) {
if (message.type == pb.Message_Type.TEST_NOTIFICATION) {
pushData = (PushNotification()..kind = PushKind.testNotification)
.writeToBuffer();
}
if (message.type == pb.Message_Type.CIPHERTEXT) {
final cipherText = await signalEncryptMessage(
receipt.contactId,
Uint8List.fromList(message.encryptedContent),
);
if (cipherText == null) {
Log.error('Could not encrypt the message. Aborting and trying again.');
return;
}
final encryptedHash = (await Sha256().hash(encryptedBytes)).bytes;
await twonlyDB.messageRetransmissionDao.updateRetransmission(
retransId,
MessageRetransmissionsCompanion(
encryptedHash: Value(Uint8List.fromList(encryptedHash)),
),
);
message.encryptedContent = cipherText.serialize();
switch (cipherText.getType()) {
case CiphertextMessage.prekeyType:
message.type = pb.Message_Type.PREKEY_BUNDLE;
case CiphertextMessage.whisperType:
message.type = pb.Message_Type.CIPHERTEXT;
default:
Log.error('Invalid ciphertext type: ${cipherText.getType()}.');
return;
}
}
final resp = await apiService.sendTextMessage(
retrans.contactId,
encryptedBytes,
retrans.pushData,
receipt.contactId,
message.writeToBuffer(),
pushData,
);
var retry = true;
if (resp.isError) {
Log.error('Could not retransmit message.');
Log.error('Could not transmit message $receiptId got ${resp.error}.');
if (resp.error == ErrorCode.UserIdNotFound) {
retry = false;
if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!,
const MessagesCompanion(errorWhileSending: Value(true)),
);
}
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
await twonlyDB.contactsDao.updateContact(
retrans.contactId,
receipt.contactId,
const ContactsCompanion(deleted: Value(true)),
);
return;
}
}
if (resp.isSuccess) {
retry = false;
if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!,
if (receipt.messageId != null) {
await twonlyDB.messagesDao.updateMessageId(
receipt.messageId!,
const MessagesCompanion(
acknowledgeByServer: Value(true),
errorWhileSending: Value(false),
ackByServer: Value(true),
),
);
}
}
if (!retry) {
if (!messageGetsAck(json.kind)) {
await twonlyDB.messageRetransmissionDao
.deleteRetransmissionById(retransId);
if (!receipt.contactWillSendsReceipt) {
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
} else {
await twonlyDB.messageRetransmissionDao.updateRetransmission(
retransId,
MessageRetransmissionsCompanion(
acknowledgeByServerAt: Value(DateTime.now()),
retryCount: Value(retrans.retryCount + 1),
await twonlyDB.receiptsDao.updateReceipt(
receiptId,
ReceiptsCompanion(
ackByServerAt: Value(DateTime.now()),
retryCount: Value(receipt.retryCount + 1),
lastRetry: Value(DateTime.now()),
),
);
}
}
} catch (e) {
Log.error('error resending message: $e');
await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
Log.error('Unknown Error when sending message: $e');
if (receiptId != null) {
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
}
if (receipt != null) {
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
}
}
}
@ -177,78 +167,31 @@ Future<void> sendCipherText(
);
if (receipt != null) {
await tryToSendCompleteMessage(receipt.receiptId);
await tryToSendCompleteMessage(receipt: receipt);
}
}
// Future<void> sendTextMessage(
// int target,
// TextMessageContent content,
// PushNotification? pushNotification,
// ) async {
// final messageSendAt = DateTime.now();
// DateTime? openedAt;
// if (pushNotification != null && pushNotification.hasReactionContent()) {
// openedAt = DateTime.now();
// }
// final messageId = await twonlyDB.messagesDao.insertMessage(
// MessagesCompanion(
// contactId: Value(target),
// kind: const Value(MessageKind.textMessage),
// sendAt: Value(messageSendAt),
// responseToOtherMessageId: Value(content.responseToMessageId),
// responseToMessageId: Value(content.responseToOtherMessageId),
// downloadState: const Value(DownloadState.downloaded),
// openedAt: Value(openedAt),
// contentJson: Value(
// jsonEncode(content.toJson()),
// ),
// ),
// );
// if (messageId == null) return;
// if (pushNotification != null && !pushNotification.hasReactionContent()) {
// pushNotification.messageId = Int64(messageId);
// }
// final msg = MessageJson(
// kind: MessageKind.textMessage,
// messageSenderId: messageId,
// content: content,
// timestamp: messageSendAt,
// );
// await encryptAndSendMessageAsync(
// messageId,
// target,
// msg,
// pushNotification: pushNotification,
// );
// }
Future<void> notifyContactAboutOpeningMessage(
int fromUserId,
List<int> messageOtherIds,
int contactId,
List<String> messageOtherIds,
) async {
var biggestMessageId = messageOtherIds.first;
for (final messageOtherId in messageOtherIds) {
if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId;
await encryptAndSendMessageAsync(
null,
fromUserId,
MessageJson(
kind: MessageKind.opened,
messageReceiverId: messageOtherId,
content: MessageContent(),
timestamp: DateTime.now(),
if (isUUIDNewer(messageOtherId, biggestMessageId)) {
biggestMessageId = messageOtherId;
}
}
await sendCipherText(
contactId,
pb.EncryptedContent(
messageUpdate: pb.EncryptedContent_MessageUpdate(
type: pb.EncryptedContent_MessageUpdate_Type.OPENED,
multipleSenderMessageIds: messageOtherIds,
),
),
);
}
await updateLastMessageId(fromUserId, biggestMessageId);
await updateLastMessageId(contactId, biggestMessageId);
}
Future<void> notifyContactsAboutProfileChange({int? onlyToContact}) async {
@ -256,11 +199,13 @@ Future<void> notifyContactsAboutProfileChange({int? onlyToContact}) async {
if (user == null) return;
if (user.avatarSvg == null) return;
final encryptedContent = pb.EncryptedContent()
..contactUpdate = (pb.EncryptedContent_ContactUpdate()
..type = pb.EncryptedContent_ContactUpdate_Type.UPDATE
..avatarSvg = user.avatarSvg!
..displayName = user.displayName);
final encryptedContent = pb.EncryptedContent(
contactUpdate: pb.EncryptedContent_ContactUpdate(
type: pb.EncryptedContent_ContactUpdate_Type.UPDATE,
avatarSvg: user.avatarSvg,
displayName: user.displayName,
),
);
if (onlyToContact != null) {
await sendCipherText(onlyToContact, encryptedContent);

View file

@ -65,7 +65,7 @@ Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
Log.info(
'Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type} for $receiptId',
);
await tryToSendCompleteMessage(receiptId);
await tryToSendCompleteMessage(receiptId: receiptId, reupload: true);
}
case Message_Type.CIPHERTEXT:
@ -97,8 +97,10 @@ Future<void> handleNewMessage(int fromUserId, Uint8List body) async {
contactWillSendsReceipt: const Value(false),
),
);
await tryToSendCompleteMessage(receiptId);
await tryToSendCompleteMessage(receiptId: receiptId);
}
case Message_Type.TEST_NOTIFICATION:
return;
}
}

View file

@ -9,6 +9,7 @@ import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
Future<void> handleContactRequest(
@ -17,6 +18,7 @@ Future<void> handleContactRequest(
) async {
switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('Got a contact request from $fromUserId');
// Request the username by the server so an attacker can not
// forge the displayed username in the contact request
final username = await apiService.getUsername(fromUserId);
@ -33,6 +35,7 @@ Future<void> handleContactRequest(
}
await setupNotificationWithUsers();
case EncryptedContent_ContactRequest_Type.ACCEPT:
Log.info('Got a contact accept from $fromUserId');
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
@ -41,6 +44,7 @@ Future<void> handleContactRequest(
),
);
case EncryptedContent_ContactRequest_Type.REJECT:
Log.info('Got a contact reject from $fromUserId');
await twonlyDB.contactsDao.deleteContactByUserId(fromUserId);
}
}
@ -48,12 +52,15 @@ Future<void> handleContactRequest(
Future<void> handleContactUpdate(
int fromUserId,
EncryptedContent_ContactUpdate contactUpdate,
int? senderProfileCounter) async {
int? senderProfileCounter,
) async {
switch (contactUpdate.type) {
case EncryptedContent_ContactUpdate_Type.REQUEST:
Log.info('Got a contact update request from $fromUserId');
await notifyContactsAboutProfileChange(onlyToContact: fromUserId);
case EncryptedContent_ContactUpdate_Type.UPDATE:
Log.info('Got a contact update $fromUserId');
if (contactUpdate.hasAvatarSvg() &&
contactUpdate.hasDisplayName() &&
senderProfileCounter != null) {
@ -74,6 +81,7 @@ Future<void> handleFlameSync(
int contactId,
EncryptedContent_FlameSync flameSync,
) async {
Log.info('Got a flameSync from $contactId');
final contact = await twonlyDB.contactsDao
.getContactByUserId(contactId)
.getSingleOrNull();

View file

@ -1,92 +1,158 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/mediafile.service.dart';
import 'package:twonly/src/services/thumbnail.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleMedia(int fromUserId, String groupId, EncryptedContent_Media media) async {
TODO
}
Future<void> handleMedia(
int fromUserId,
String groupId,
EncryptedContent_Media media,
) async {
Log.info(
'Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}',
);
Future<void> handleMediaUpdate(int fromUserId, String groupId, EncryptedContent_MediaUpdate mediaUpdate) async {
TODO
late MediaType mediaType;
switch (media.type) {
case EncryptedContent_Media_Type.REUPLOAD:
final message = await twonlyDB.messagesDao
.getMessageById(media.senderMessageId)
.getSingleOrNull();
if (message == null ||
message.senderId != fromUserId ||
message.mediaId == null) {
return;
}
// in case there was already a downloaded file delete it...
await removeMediaFile(message.mediaId!);
// switch (message.kind) {
// case MessageKind.receiveMediaError:
// if (message.messageReceiverId != null) {
// final openedMessage = await twonlyDB.messagesDao
// .getMessageByIdAndContactId(fromUserId, message.messageReceiverId!)
// .getSingleOrNull();
// if (openedMessage != null) {
// /// message found
// /// checks if
// /// 1. this was a media upload
// /// 2. the media was not already retransmitted
// /// 3. the media was send in the last two days
// if (openedMessage.mediaUploadId != null &&
// openedMessage.mediaRetransmissionState ==
// MediaRetransmitting.none &&
// openedMessage.sendAt
// .isAfter(DateTime.now().subtract(const Duration(days: 2)))) {
// // reset the media upload state to pending,
// // this will cause the media to be re-encrypted again
// await twonlyDB.mediaUploadsDao.updateMediaUpload(
// openedMessage.mediaUploadId!,
// const MediaUploadsCompanion(
// state: Value(
// UploadState.pending,
// ),
// ),
// );
// // reset the message upload so the upload will be done again
// await twonlyDB.messagesDao.updateMessageByOtherUser(
// fromUserId,
// message.messageReceiverId!,
// const MessagesCompanion(
// downloadState: Value(DownloadState.pending),
// mediaRetransmissionState:
// Value(MediaRetransmitting.retransmitted),
// ),
// );
// unawaited(retryMediaUpload(false));
// } else {
// await twonlyDB.messagesDao.updateMessageByOtherUser(
// fromUserId,
// message.messageReceiverId!,
// const MessagesCompanion(
// errorWhileSending: Value(true),
// ),
// );
// }
// }
// }
if (message.kind == MessageKind.storedMediaFile) {
if (message.messageReceiverId != null) {
/// stored media file just updates the message
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageReceiverId!,
const MessagesCompanion(
mediaStored: Value(true),
errorWhileSending: Value(false),
await twonlyDB.mediaFilesDao.updateMedia(
message.mediaId!,
MediaFilesCompanion(
downloadState: const Value(DownloadState.pending),
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
),
);
final msg = await twonlyDB.messagesDao
.getMessageByIdAndContactId(
final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
if (mediaFile != null) {
unawaited(startDownloadMedia(mediaFile, false));
}
return;
case EncryptedContent_Media_Type.IMAGE:
mediaType = MediaType.image;
case EncryptedContent_Media_Type.VIDEO:
mediaType = MediaType.video;
case EncryptedContent_Media_Type.GIF:
mediaType = MediaType.gif;
}
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
downloadState: const Value(DownloadState.pending),
type: Value(mediaType),
requiresAuthentication: Value(media.requiresAuthentication),
displayLimitInMilliseconds: Value(
media.hasDisplayLimitInMilliseconds()
? media.displayLimitInMilliseconds.toInt()
: null,
),
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
createdAt: Value(fromTimestamp(media.timestamp)),
),
);
if (mediaFile == null) {
return;
}
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: Value(media.senderMessageId),
senderId: Value(fromUserId),
groupId: Value(groupId),
mediaId: Value(mediaFile.mediaId),
ackByServer: const Value(true),
ackByUser: const Value(true),
quotesMessageId: Value(
media.hasQuoteMessageId() ? media.quoteMessageId : null,
),
createdAt: Value(fromTimestamp(media.timestamp)),
),
);
if (message != null) {
Log.info('Inserted a new media message with ID: ${message.messageId}');
await twonlyDB.contactsDao.incFlameCounter(
fromUserId,
message.messageReceiverId!,
)
.getSingleOrNull();
if (msg != null && msg.mediaUploadId != null) {
final filePath = await getMediaFilePath(msg.mediaUploadId, 'send');
if (filePath.contains('mp4')) {
unawaited(createThumbnailsForVideo(File(filePath)));
} else {
unawaited(createThumbnailsForImage(File(filePath)));
true,
fromTimestamp(media.timestamp),
);
unawaited(startDownloadMedia(mediaFile, false));
}
}
Future<void> handleMediaUpdate(
int fromUserId,
String groupId,
EncryptedContent_MediaUpdate mediaUpdate,
) async {
final message = await twonlyDB.messagesDao
.getMessageById(mediaUpdate.targetMessageId)
.getSingleOrNull();
if (message == null || message.mediaId == null) return;
final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
if (mediaFile == null) return;
switch (mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED:
Log.info('Got media file reopened ${mediaFile.mediaId}');
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
reopenByContact: Value(true),
),
);
case EncryptedContent_MediaUpdate_Type.STORED:
Log.info('Got media file stored ${mediaFile.mediaId}');
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
storedByContact: Value(true),
),
);
unawaited(createThumbnailForMediaFile(mediaFile));
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}');
final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? [];
reuploadRequestedBy.add(fromUserId);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
uploadState: const Value(UploadState.pending),
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);
}
}
}
} else if (message.content != null) {}
}

View file

@ -10,12 +10,15 @@ Future<void> handleMessageUpdate(
) async {
switch (messageUpdate.type) {
case EncryptedContent_MessageUpdate_Type.OPENED:
Log.info('Opened message ${messageUpdate.senderMessageId}');
Log.info(
'Opened message ${messageUpdate.multipleSenderMessageIds.length}');
for (final senderMessageId in messageUpdate.multipleSenderMessageIds) {
await twonlyDB.messagesDao.handleMessageOpened(
groupId,
messageUpdate.senderMessageId,
senderMessageId,
fromTimestamp(messageUpdate.timestamp),
);
}
case EncryptedContent_MessageUpdate_Type.DELETE:
Log.info('Delete message ${messageUpdate.senderMessageId}');
await twonlyDB.messagesDao.handleMessageDeletion(

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart';
DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
@ -11,6 +12,7 @@ Future<void> handlePushKey(
) async {
switch (pushKeys.type) {
case EncryptedContent_PushKeys_Type.REQUEST:
Log.info('Got a pushkey request from $contactId');
if (lastPushKeyRequest
.isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) {
lastPushKeyRequest = DateTime.now();
@ -18,6 +20,7 @@ Future<void> handlePushKey(
}
case EncryptedContent_PushKeys_Type.UPDATE:
Log.info('Got a pushkey update from $contactId');
await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key);
}
}

View file

@ -1,11 +1,13 @@
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleReaction(
int fromUserId,
String groupId,
EncryptedContent_Reaction reaction,
) async {
Log.info('Got a reaction from $fromUserId');
if (reaction.hasRemove()) {
if (reaction.remove) {
await twonlyDB.reactionsDao

View file

@ -1,125 +1,34 @@
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleTextMessage(
int fromUserId,
String groupId,
EncryptedContent_TextMessage textMessage,
) async {
TODO
// final content = message.content!;
// // when a message is received doubled ignore it...
Log.info(
'Got a text message: ${textMessage.senderMessageId} from $groupId',
);
// final openedMessage = await twonlyDB.messagesDao
// .getMessageByOtherMessageId(fromUserId, message.messageSenderId!)
// .getSingleOrNull();
// if (openedMessage != null) {
// if (openedMessage.errorWhileSending) {
// await twonlyDB.messagesDao
// .deleteMessagesByMessageId(openedMessage.messageId);
// } else {
// Log.error(
// 'Got a duplicated message from other user: ${message.messageSenderId!}',
// );
// final ok = client.Response_Ok()..none = true;
// return client.Response()..ok = ok;
// }
// }
// int? responseToMessageId;
// int? responseToOtherMessageId;
// int? messageId;
// var acknowledgeByUser = false;
// DateTime? openedAt;
// if (message.kind == MessageKind.reopenedMedia) {
// acknowledgeByUser = true;
// openedAt = DateTime.now();
// }
// if (content is TextMessageContent) {
// responseToMessageId = content.responseToMessageId;
// responseToOtherMessageId = content.responseToOtherMessageId;
// if (responseToMessageId != null || responseToOtherMessageId != null) {
// // reactions are shown in the notification directly...
// if (isEmoji(content.text)) {
// openedAt = DateTime.now();
// }
// }
// }
// if (content is ReopenedMediaFileContent) {
// responseToMessageId = content.messageId;
// }
// if (responseToMessageId != null) {
// await twonlyDB.messagesDao.updateMessageByOtherUser(
// fromUserId,
// responseToMessageId,
// MessagesCompanion(
// errorWhileSending: const Value(false),
// openedAt: Value(
// DateTime.now(),
// ), // when a user reacted to the media file, it should be marked as opened
// ),
// );
// }
// final contentJson = jsonEncode(content.toJson());
// final update = MessagesCompanion(
// contactId: Value(fromUserId),
// kind: Value(message.kind),
// messageOtherId: Value(message.messageSenderId),
// contentJson: Value(contentJson),
// acknowledgeByServer: const Value(true),
// acknowledgeByUser: Value(acknowledgeByUser),
// responseToMessageId: Value(responseToMessageId),
// responseToOtherMessageId: Value(responseToOtherMessageId),
// openedAt: Value(openedAt),
// downloadState: Value(
// message.kind == MessageKind.media
// ? DownloadState.pending
// : DownloadState.downloaded,
// ),
// sendAt: Value(message.timestamp),
// );
// messageId = await twonlyDB.messagesDao.insertMessage(
// update,
// );
// if (messageId == null) {
// Log.error('could not insert message into db');
// return client.Response()..error = ErrorCode.InternalError;
// }
// Log.info('Inserted a new message with id: $messageId');
// if (message.kind == MessageKind.media) {
// await twonlyDB.contactsDao.incFlameCounter(
// fromUserId,
// true,
// message.timestamp,
// );
// final msg = await twonlyDB.messagesDao
// .getMessageByMessageId(messageId)
// .getSingleOrNull();
// if (msg != null) {
// unawaited(startDownloadMedia(msg, false));
// }
// }
// } else {
// Log.error('Content is not defined $message');
// }
// // unarchive contact when receiving a new message
// await twonlyDB.contactsDao.updateContact(
// fromUserId,
// const ContactsCompanion(
// archived: Value(false),
// ),
// );
// return null;
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: Value(textMessage.senderMessageId),
senderId: Value(fromUserId),
groupId: Value(groupId),
content: Value(textMessage.text),
ackByServer: const Value(true),
ackByUser: const Value(true),
quotesMessageId: Value(
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
),
createdAt: Value(fromTimestamp(textMessage.timestamp)),
),
);
if (message != null) {
Log.info('Inserted a new text message with ID: ${message.messageId}');
}
}

View file

@ -7,7 +7,7 @@ import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pbenum.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'

View file

@ -7,15 +7,17 @@ import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart' as my;
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
/// This function must be called after the database is setup
Future<void> setupNotificationWithUsers({
@ -104,19 +106,14 @@ Future<void> setupNotificationWithUsers({
}
Future<void> sendNewPushKey(int userId, PushKey pushKey) async {
await encryptAndSendMessageAsync(
null,
await sendCipherText(
userId,
my.MessageJson(
kind: MessageKind.pushKey,
content: my.PushKeyContent(
keyId: pushKey.id.toInt(),
key: pushKey.key,
),
timestamp: DateTime.fromMillisecondsSinceEpoch(
pushKey.createdAtUnixTimestamp.toInt(),
),
),
EncryptedContent()
..pushKeys = (EncryptedContent_PushKeys()
..type = EncryptedContent_PushKeys_Type.UPDATE
..key = pushKey.key
..keyId = pushKey.id
..createdAt = pushKey.createdAtUnixTimestamp),
);
}
@ -132,7 +129,7 @@ Future<void> updatePushUser(Contact contact) async {
displayName: getContactDisplayName(contact),
pushKeys: [],
blocked: contact.blocked,
lastMessageId: Int64(),
lastMessageId: uuid.v7(),
),
);
} else {
@ -160,7 +157,7 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
displayName: getContactDisplayName(contact),
pushKeys: [],
blocked: contact.blocked,
lastMessageId: Int64(),
lastMessageId: uuid.v7(),
),
);
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
@ -174,8 +171,8 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
pushUser!.pushKeys.clear();
pushUser.pushKeys.add(
PushKey(
id: Int64(pushKey.keyId),
key: pushKey.key,
id: Int64(keyId),
key: key,
createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch),
),
);
@ -183,7 +180,7 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys);
}
Future<void> updateLastMessageId(int fromUserId, int messageId) async {
Future<void> updateLastMessageId(int fromUserId, String messageId) async {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
@ -192,15 +189,103 @@ Future<void> updateLastMessageId(int fromUserId, int messageId) async {
return;
}
if (pushUser.lastMessageId < Int64(messageId)) {
pushUser.lastMessageId = Int64(messageId);
if (isUUIDNewer(messageId, pushUser.lastMessageId)) {
pushUser.lastMessageId = messageId;
await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers);
}
}
Future<Uint8List?> getPushDataFromEncryptedContent(
int toUserId,
String? messageId,
EncryptedContent content,
) async {
late PushKind kind;
String? reactionContent;
if (content.hasReaction()) {
if (content.reaction.remove) return null;
final msg = await twonlyDB.messagesDao
.getMessageById(content.reaction.targetMessageId)
.getSingleOrNull();
if (msg == null) return null;
if (msg.content != null) {
kind = PushKind.reactionToText;
} else if (msg.mediaId != null) {
final media = await twonlyDB.mediaFilesDao.getMediaFileById(msg.mediaId!);
if (media == null) return null;
switch (media.type) {
case MediaType.image:
kind = PushKind.reactionToImage;
case MediaType.video:
kind = PushKind.reactionToVideo;
case MediaType.gif:
kind = PushKind.reaction;
}
}
reactionContent = content.reaction.emoji;
}
if (content.hasTextMessage()) {
kind = PushKind.text;
if (content.textMessage.hasQuoteMessageId()) {
kind = PushKind.response;
}
}
if (content.hasMedia()) {
switch (content.media.type) {
case EncryptedContent_Media_Type.IMAGE:
kind = PushKind.image;
case EncryptedContent_Media_Type.VIDEO:
kind = PushKind.video;
// ignore: no_default_cases
default:
return null;
}
if (content.media.requiresAuthentication) {
kind = PushKind.twonly;
}
}
if (content.hasContactRequest()) {
switch (content.contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
kind = PushKind.contactRequest;
case EncryptedContent_ContactRequest_Type.ACCEPT:
kind = PushKind.acceptRequest;
case EncryptedContent_ContactRequest_Type.REJECT:
return null;
}
}
if (content.hasMediaUpdate()) {
switch (content.mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED:
kind = PushKind.reopenedMedia;
case EncryptedContent_MediaUpdate_Type.STORED:
kind = PushKind.storedMediaFile;
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
return null;
}
}
final pushNotification = PushNotification()..kind = kind;
if (reactionContent != null) {
pushNotification.reactionContent = reactionContent;
}
if (messageId != null) {
pushNotification.messageId = messageId;
}
return encryptPushNotification(toUserId, pushNotification);
}
/// this will trigger a push notification
/// push notification only containing the message kind and username
Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
Future<Uint8List?> encryptPushNotification(
int toUserId,
PushNotification content,
) async {
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
var key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
@ -218,14 +303,12 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
// this will be enforced after every app uses this system... :/
// return null;
Log.error('Using insecure key as the receiver does not send a push key!');
await encryptAndSendMessageAsync(
null,
await sendCipherText(
toUserId,
my.MessageJson(
kind: MessageKind.requestPushKey,
content: my.MessageContent(),
timestamp: DateTime.now(),
),
EncryptedContent()
..pushKeys = (EncryptedContent_PushKeys()
..type = EncryptedContent_PushKeys_Type.REQUEST),
);
}
} else {

View file

@ -7,16 +7,15 @@ import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/prekeys.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
/// This caused some troubles, so protection the encryption...
final lockingSignalEncryption = Mutex();
Future<Uint8List?> signalEncryptMessage(
Future<CiphertextMessage?> signalEncryptMessage(
int target,
Uint8List plaintextContent,
) async {
return lockingSignalEncryption.protect<Uint8List?>(() async {
return lockingSignalEncryption.protect<CiphertextMessage?>(() async {
try {
final signalStore = (await getSignalStore())!;
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
@ -75,14 +74,7 @@ Future<Uint8List?> signalEncryptMessage(
Log.error('did not get the identity of the remote address');
}
}
final ciphertext = await session.encrypt(plaintextContent);
final b = BytesBuilder()
..add(ciphertext.serialize())
..add(intToBytes(ciphertext.getType()));
return b.takeBytes();
return await session.encrypt(plaintextContent);
} catch (e) {
Log.error(e.toString());
return null;

View file

@ -2,9 +2,23 @@ import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path/path.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
Future<void> createThumbnailForMediaFile(MediaFile media) async {
switch (media.type) {
case MediaType.image:
TODO
break;
default:
}
}
Future<void> createThumbnailsForImage(File file) async {
final fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != 'png') {

View file

@ -267,3 +267,9 @@ MediaMessageContent? getMediaContent(Message message) {
return null;
}
}
bool isUUIDNewer(String uuid1, String uuid2) {
final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
return timestamp1 > timestamp2;
}

View file

@ -1,13 +1,12 @@
import 'dart:io';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/misc.dart';
@ -52,10 +51,10 @@ class NotificationView extends StatelessWidget {
if (run) {
final user = await getUser();
if (user != null) {
final pushData = await getPushData(
final pushData = await encryptPushNotification(
user.userId,
PushNotification(
messageId: Int64(),
messageId: uuid.v4(),
kind: PushKind.testNotification,
),
);

View file

@ -1,9 +1,11 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
@ -40,5 +42,11 @@ void main() {
final uv4String = utf8.decode(uv4Bytes.cast<int>());
expect(uv4String, uv4);
});
test('comparing uui7', () async {
final uv7Old = uuid.v7();
sleep(const Duration(milliseconds: 1000));
final uv7New = uuid.v7();
expect(isUUIDNewer(uv7New, uv7Old), true);
});
});
}