This commit is contained in:
otsmr 2025-03-31 21:59:57 +02:00
parent b756682f7c
commit bf34920350
10 changed files with 207 additions and 139 deletions

View file

@ -33,7 +33,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "eu.twonly.testng" applicationId = "eu.twonly.testing"
multiDexEnabled true multiDexEnabled true
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.

View file

@ -12,10 +12,10 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
951DB0F2008EB94699D02555 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F3BB8E3AC9CEA61248BD989 /* libPods-Runner.a */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A8E4DD3B3139A6996AC817E0 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F3BB8E3AC9CEA61248BD989 /* libPods-Runner.a */; };
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -83,7 +83,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A8E4DD3B3139A6996AC817E0 /* libPods-Runner.a in Frameworks */, 951DB0F2008EB94699D02555 /* libPods-Runner.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View file

@ -1,3 +1,4 @@
arb-dir: lib/src/localization arb-dir: lib/src/localization
template-arb-file: app_en.arb template-arb-file: app_en.arb
output-localization-file: app_localizations.dart output-localization-file: app_localizations.dart
untranslated-messages-file: build/l10n.log

View file

@ -144,11 +144,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
} }
Future<bool> containsOtherMessageId(int messageOtherId) async { Future<bool> containsOtherMessageId(
int fromUserId, int messageOtherId) async {
final query = select(messages) final query = select(messages)
..where((t) => t.messageOtherId.equals(messageOtherId)); ..where((t) =>
final entry = await query.getSingleOrNull(); t.messageOtherId.equals(messageOtherId) &
return entry != null; t.contactId.equals(fromUserId));
final entry = await query.get();
return entry.isNotEmpty;
} }
SingleOrNullSelectable<Message> getMessageByMessageId(int messageId) { SingleOrNullSelectable<Message> getMessageByMessageId(int messageId) {

View file

@ -43,6 +43,7 @@
"contextMenuVerifyUser": "Kontakt verifizieren", "contextMenuVerifyUser": "Kontakt verifizieren",
"contextMenuOpenChat": "Chat öffnen", "contextMenuOpenChat": "Chat öffnen",
"contextMenuSendImage": "Bild senden", "contextMenuSendImage": "Bild senden",
"mediaViewerAuthReason": "Bitte authentifiziere dich, um diesen twonly zu sehen!",
"messageSendState_Received": "Empfangen", "messageSendState_Received": "Empfangen",
"messageSendState_Opened": "Geöffnet", "messageSendState_Opened": "Geöffnet",
"messageSendState_Send": "Gesendet", "messageSendState_Send": "Gesendet",

View file

@ -1,40 +1,79 @@
{ {
"@@locale": "en", "@@locale": "en",
"registerTitle": "Welcome to twonly!", "registerTitle": "Welcome to twonly!",
"@registerTitle": {},
"registerSlogan": "twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing", "registerSlogan": "twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing",
"@registerSlogan": {},
"onboardingWelcomeTitle": "Welcome to twonly!", "onboardingWelcomeTitle": "Welcome to twonly!",
"@onboardingWelcomeTitle": {},
"onboardingWelcomeBody": "Experience a private and secure way to stay in touch with friends by sharing instant pictures.", "onboardingWelcomeBody": "Experience a private and secure way to stay in touch with friends by sharing instant pictures.",
"@onboardingWelcomeBody": {},
"onboardingE2eTitle": "Carefree sharing", "onboardingE2eTitle": "Carefree sharing",
"@onboardingE2eTitle": {},
"onboardingE2eBody": "With end-to-end encryption, enjoy the peace of mind that only you and your friends can see the moments you share.", "onboardingE2eBody": "With end-to-end encryption, enjoy the peace of mind that only you and your friends can see the moments you share.",
"@onboardingE2eBody": {},
"onboardingFocusTitle": "Focus on sharing moments", "onboardingFocusTitle": "Focus on sharing moments",
"@onboardingFocusTitle": {},
"onboardingFocusBody": "Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.", "onboardingFocusBody": "Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.",
"@onboardingFocusBody": {},
"onboardingSendTwonliesTitle": "Send twonlies", "onboardingSendTwonliesTitle": "Send twonlies",
"@onboardingSendTwonliesTitle": {},
"onboardingSendTwonliesBody": "Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!", "onboardingSendTwonliesBody": "Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!",
"@onboardingSendTwonliesBody": {},
"onboardingNotProductTitle": "You are not the product!", "onboardingNotProductTitle": "You are not the product!",
"@onboardingNotProductTitle": {},
"onboardingNotProductBody": "twonly is financed by a small monthly fee and not by selling your data.", "onboardingNotProductBody": "twonly is financed by a small monthly fee and not by selling your data.",
"@onboardingNotProductBody": {},
"onboardingBuyOneGetTwoTitle": "Buy one get two", "onboardingBuyOneGetTwoTitle": "Buy one get two",
"@onboardingBuyOneGetTwoTitle": {},
"onboardingBuyOneGetTwoBody": "twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.", "onboardingBuyOneGetTwoBody": "twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.",
"@onboardingBuyOneGetTwoBody": {},
"onboardingGetStartedTitle": "Let's go!", "onboardingGetStartedTitle": "Let's go!",
"@onboardingGetStartedTitle": {},
"onboardingGetStartedBody": "You can test twonly free of charge for 14 days, after that it costs either 1€/month or 9€/year.", "onboardingGetStartedBody": "You can test twonly free of charge for 14 days, after that it costs either 1€/month or 9€/year.",
"@onboardingGetStartedBody": {},
"onboardingTryForFree": "Try for free", "onboardingTryForFree": "Try for free",
"@onboardingTryForFree": {},
"registerUsernameSlogan": "Please select a username so others can find you!", "registerUsernameSlogan": "Please select a username so others can find you!",
"@registerUsernameSlogan": {},
"registerUsernameDecoration": "Username", "registerUsernameDecoration": "Username",
"@registerUsernameDecoration": {},
"registerUsernameLimits": "Username must be 3 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).", "registerUsernameLimits": "Username must be 3 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).",
"@registerUsernameLimits": {},
"registerSubmitButton": "Register now!", "registerSubmitButton": "Register now!",
"@registerSubmitButton": {},
"newMessageTitle": "New message", "newMessageTitle": "New message",
"@newMessageTitle": {},
"chatsTapToSend": "Click to send your first image", "chatsTapToSend": "Click to send your first image",
"@chatsTapToSend": {},
"shareImageTitle": "Share with", "shareImageTitle": "Share with",
"@shareImageTitle": {},
"shareImageBestFriends": "Best friends", "shareImageBestFriends": "Best friends",
"@shareImageBestFriends": {},
"shareImagedEditorSendImage": "Send", "shareImagedEditorSendImage": "Send",
"@shareImagedEditorSendImage": {},
"shareImagedEditorShareWith": "Share with", "shareImagedEditorShareWith": "Share with",
"@shareImagedEditorShareWith": {},
"shareImagedEditorSaveImage": "Save", "shareImagedEditorSaveImage": "Save",
"@shareImagedEditorSaveImage": {},
"shareImagedEditorSavedImage": "Saved", "shareImagedEditorSavedImage": "Saved",
"@shareImagedEditorSavedImage": {},
"shareImageAllUsers": "All contacts", "shareImageAllUsers": "All contacts",
"@shareImageAllUsers": {},
"shareImageAllTwonlyWarning": "twonlies can only be send to verified contacts!", "shareImageAllTwonlyWarning": "twonlies can only be send to verified contacts!",
"@shareImageAllTwonlyWarning": {},
"searchUsernameInput": "Username", "searchUsernameInput": "Username",
"@searchUsernameInput": {},
"searchUsernameTitle": "Search username", "searchUsernameTitle": "Search username",
"@searchUsernameTitle": {},
"searchUsernameNotFound": "Username not found", "searchUsernameNotFound": "Username not found",
"searchUsernameNotFoundBody": "There is no user with the username \"{username}\" registered.", "@searchUsernameNotFound": {},
"searchUsernameNotFoundBody": "There is no user with the username \"{username}\" registered",
"@searchUsernameNotFoundBody": {
"placeholders": {
"username": {}
}
},
"searchUsernameNewFollowerTitle": "Follow requests", "searchUsernameNewFollowerTitle": "Follow requests",
"searchUsernameQrCodeBtn": "Scan QR code", "searchUsernameQrCodeBtn": "Scan QR code",
"chatListViewSearchUserNameBtn": "Add your first twonly contact!", "chatListViewSearchUserNameBtn": "Add your first twonly contact!",

View file

@ -162,28 +162,18 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async { Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
MessageJson? message = await SignalHelper.getDecryptedText(fromUserId, body); MessageJson? message = await SignalHelper.getDecryptedText(fromUserId, body);
if (message != null) { if (message == null) {
Logger("server_messages")
.info("Got invalid cypher text from $fromUserId. Deleting it.");
// Message is not valid, so server can delete it
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
switch (message.kind) { switch (message.kind) {
case MessageKind.contactRequest: case MessageKind.contactRequest:
Result username = await apiProvider.getUsername(fromUserId); return handleContactRequest(fromUserId, message);
if (username.isSuccess) {
Uint8List name = username.value.userdata.username;
int added =
await twonlyDatabase.contactsDao.insertContact(ContactsCompanion(
username: Value(utf8.decode(name)),
userId: Value(fromUserId),
requested: Value(true),
));
if (added > 0) {
localPushNotificationNewMessage(
fromUserId.toInt(),
message,
999999,
);
}
}
break;
case MessageKind.opened: case MessageKind.opened:
final update = MessagesCompanion(openedAt: Value(message.timestamp)); final update = MessagesCompanion(openedAt: Value(message.timestamp));
await twonlyDatabase.messagesDao.updateMessageByOtherUser( await twonlyDatabase.messagesDao.updateMessageByOtherUser(
@ -192,15 +182,18 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
update, update,
); );
break; break;
case MessageKind.rejectRequest: case MessageKind.rejectRequest:
await twonlyDatabase.contactsDao.deleteContactByUserId(fromUserId); await twonlyDatabase.contactsDao.deleteContactByUserId(fromUserId);
break; break;
case MessageKind.acceptRequest: case MessageKind.acceptRequest:
final update = ContactsCompanion(accepted: Value(true)); final update = ContactsCompanion(accepted: Value(true));
await twonlyDatabase.contactsDao.updateContact(fromUserId, update); await twonlyDatabase.contactsDao.updateContact(fromUserId, update);
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888); localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
notifyContactsAboutProfileChange(); notifyContactsAboutProfileChange();
break; break;
case MessageKind.profileChange: case MessageKind.profileChange:
var content = message.content; var content = message.content;
if (content is ProfileContent) { if (content is ProfileContent) {
@ -211,6 +204,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
twonlyDatabase.contactsDao.updateContact(fromUserId, update); twonlyDatabase.contactsDao.updateContact(fromUserId, update);
} }
break; break;
case MessageKind.ack: case MessageKind.ack:
final update = MessagesCompanion(acknowledgeByUser: Value(true)); final update = MessagesCompanion(acknowledgeByUser: Value(true));
await twonlyDatabase.messagesDao.updateMessageByOtherUser( await twonlyDatabase.messagesDao.updateMessageByOtherUser(
@ -219,17 +213,29 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
update, update,
); );
break; break;
default: default:
if (message.kind != MessageKind.textMessage && if (message.kind != MessageKind.textMessage &&
message.kind != MessageKind.media && message.kind != MessageKind.media &&
message.kind != MessageKind.storedMediaFile) { message.kind != MessageKind.storedMediaFile) {
Logger("handleServerMessages") Logger("handleServerMessages")
.shout("Got unknown MessageKind $message"); .shout("Got unknown MessageKind $message");
} else if (message.content != null && message.messageId != null) { } else if (message.content == null || message.messageId == null) {
Logger("handleServerMessages")
.shout("Content or messageid not defined $message");
} else {
// when a message is received doubled ignore it...
if ((await twonlyDatabase.messagesDao
.containsOtherMessageId(fromUserId, message.messageId!))) {
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
String content = jsonEncode(message.content!.toJson()); String content = jsonEncode(message.content!.toJson());
bool acknowledgeByUser = false; bool acknowledgeByUser = false;
DateTime? openedAt; DateTime? openedAt;
if (message.kind == MessageKind.storedMediaFile) { if (message.kind == MessageKind.storedMediaFile) {
acknowledgeByUser = true; acknowledgeByUser = true;
openedAt = DateTime.now(); openedAt = DateTime.now();
@ -241,13 +247,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
responseToMessageId = textContent.responseToMessageId; responseToMessageId = textContent.responseToMessageId;
} }
// when a message is received doubled ignore it...
if ((await twonlyDatabase.messagesDao
.containsOtherMessageId(message.messageId!))) {
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
final update = MessagesCompanion( final update = MessagesCompanion(
contactId: Value(fromUserId), contactId: Value(fromUserId),
kind: Value(message.kind), kind: Value(message.kind),
@ -303,7 +302,6 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
localPushNotificationNewMessage(fromUserId, message, messageId); localPushNotificationNewMessage(fromUserId, message, messageId);
} }
} }
}
var ok = client.Response_Ok()..none = true; var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
@ -321,3 +319,31 @@ Future<client.Response> handleRequestNewPreKey() async {
var ok = client.Response_Ok()..prekeys = prekeys; var ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
Future<client.Response> handleContactRequest(
int fromUserId, MessageJson message) async {
// request the username by the server so an attacker can not
// forge the displayed username in the contact request
Result username = await apiProvider.getUsername(fromUserId);
if (username.isSuccess) {
Uint8List name = username.value.userdata.username;
int added = await twonlyDatabase.contactsDao.insertContact(
ContactsCompanion(
username: Value(utf8.decode(name)),
userId: Value(fromUserId),
requested: Value(true),
),
);
if (added > 0) {
localPushNotificationNewMessage(
fromUserId,
message,
999999,
);
}
}
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}

View file

@ -279,9 +279,6 @@ Future<MessageJson?> getDecryptedText(int source, Uint8List msg) async {
if (msgs == null) return null; if (msgs == null) return null;
Uint8List body = msgs[0]; Uint8List body = msgs[0];
int type = bytesToInt(msgs[1]); int type = bytesToInt(msgs[1]);
// gzip.decode(body);
Uint8List plaintext; Uint8List plaintext;
if (type == CiphertextMessage.prekeyType) { if (type == CiphertextMessage.prekeyType) {
PreKeySignalMessage pre = PreKeySignalMessage(body); PreKeySignalMessage pre = PreKeySignalMessage(body);

View file

@ -426,7 +426,7 @@ packages:
source: hosted source: hosted
version: "3.10.4" version: "3.10.4"
fixnum: fixnum:
dependency: transitive dependency: "direct main"
description: description:
name: fixnum name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be

View file

@ -55,6 +55,7 @@ dependencies:
avatar_maker: ^0.2.0 avatar_maker: ^0.2.0
flutter_svg: ^2.0.17 flutter_svg: ^2.0.17
flutter_volume_controller: ^1.3.3 flutter_volume_controller: ^1.3.3
fixnum: ^1.1.1
# avatar_maker # avatar_maker
# avatar_maker: # avatar_maker:
# path: ./dependencies/avatar_maker/ # path: ./dependencies/avatar_maker/