enabling linter

This commit is contained in:
otsmr 2025-07-15 00:38:23 +02:00
parent 0eda03537c
commit a3d9bcf98b
148 changed files with 4643 additions and 4406 deletions

271
.vscode/launch.json vendored
View file

@ -1,271 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "twonly-app",
"request": "launch",
"type": "dart"
},
{
"name": "twonly-app (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "twonly-app (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage",
"cwd": "dependencies/flutter_secure_storage",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_zxing",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_zxing (profile mode)",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_zxing (release mode)",
"cwd": "dependencies/flutter_zxing",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_darwin",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_darwin (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_darwin (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_darwin",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_linux",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_linux (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_linux (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_linux",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_platform_interface",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_platform_interface (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_platform_interface (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_platform_interface",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_web",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_web (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_web (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_web",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_windows",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_windows (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_windows (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_zxing/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "zxscanner",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart"
},
{
"name": "zxscanner (profile mode)",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "zxscanner (release mode)",
"cwd": "dependencies/flutter_zxing/zxscanner",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "flutter_secure_storage_macos",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart"
},
{
"name": "flutter_secure_storage_macos (profile mode)",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "flutter_secure_storage_macos (release mode)",
"cwd": "dependencies/flutter_secure_storage/archived_packages/flutter_secure_storage_macos",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "dependencies/flutter_secure_storage/flutter_secure_storage_windows/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

View file

@ -1,5 +0,0 @@
{
"files.exclude": {
"dependencies": false
}
}

View file

@ -13,12 +13,15 @@ This repository contains the complete source code of the [twonly](https://twonly
## In work
- We plan to implement a Sealed Sender feature to minimize metadata
- We currently evaluating to switch from the Signal Protocol to [MLS](https://openmls.tech/).
- For Android: Using [UnifiedPush](https://unifiedpush.org/) instead of FCM
- For Android: Reproducible Builds + Publishing on Github/F-Droid
- Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata
- Maybe: Switching from the Signal Protocol to [MLS](https://openmls.tech/).
## Security Issues
If you discover a security issue in twonly, please adhere to the coordinated vulnerability disclosure model. Please send us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not guarantee a bounty currently :/
If you discover a security issue in twonly, please adhere to the coordinated vulnerability disclosure model. Please send
us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not
guarantee a bounty currently :/
## Development

View file

@ -1,28 +1,24 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
include: package:very_good_analysis/analysis_options.yaml
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
always_use_package_imports: ignore
public_member_api_docs: ignore
lines_longer_than_80_chars: ignore
avoid_function_literals_in_foreach_calls: ignore
avoid_setters_without_getters: ignore
inference_failure_on_instance_creation: ignore
avoid_positional_boolean_parameters: ignore
inference_failure_on_collection_literal: ignore
exclude:
- "lib/src/model/protobuf/**"
- "lib/src/model/protobuf/api/websocket/**"
- "lib/generated/**"
- "test/drift/**"
- "**.g.dart"
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
public_member_api_docs: false
avoid_catches_without_on_clauses: false
document_ignores: false

View file

@ -1,175 +0,0 @@
emojies = {
"": "red_heart.json",
"😂": "joy.json",
"🔥": "fire.json",
"💪": "muscle.json",
"😭": "loudly-crying.json",
"🤯": "mind-blown.json",
"❤️‍🔥": "red_heart_fire.json",
"😁": "grinning.json",
"😆": "laughing.json",
"😅": "grin-sweat.json",
"🤣": "rofl.json",
"😉": "wink.json",
"😘": "kissing-heart.json",
"🥰": "heart-face.json",
"😍": "heart-eyes.json",
"🤩": "star-struck.json",
"🥳": "partying-face.json",
"🙃": "upside-down-face.json",
"🥲": "happy-cry.json",
"😊": "blush.json",
"😏": "smirk.json",
"🤤": "drool.json",
"😋": "yum.json",
"😛": "stuck-out-tongue.json",
"🤪": "zany-face.json",
"🥴": "woozy.json",
"😔": "pensive.json",
"🥺": "pleading.json",
"😬": "grimacing.json",
"😑": "expressionless.json",
"🤐": "zipper-face.json",
"🤔": "thinking-face.json",
"🥱": "yawn.json",
"🤗": "hug-face.json",
"😱": "screaming.json",
"🤨": "raised-eyebrow.json",
"🧐": "monocle.json",
"😒": "unamused.json",
"🙄": "rolling-eyes.json",
"😤": "triumph.json",
"🤬": "cursing.json",
"😞": "sad.json",
"😢": "cry.json",
"🙁": "frown.json",
"😨": "scared.json",
"😳": "flushed.json",
"😖": "scrunched-mouth.json",
"😵": "x-eyes.json",
"🥶": "cold-face.json",
"🥵": "hot-face.json",
"🤮": "vomit.json",
"😴": "sleep.json",
"🤒": "thermometer-face.json",
"🤕": "bandage-face.json",
"🤥": "liar.json",
"😇": "halo.json",
"🤠": "cowboy.json",
"🤑": "money-face.json",
"🤓": "nerd-face.json",
"😎": "sunglasses-face.json",
"🥸": "disguise.json",
"🤡": "clown.json",
"💩": "poop.json",
"😈": "imp-smile.json",
"👻": "ghost.json",
"💀": "skull.json",
"": "snowman.json",
"🎃": "jack-o-lantern.json",
"🤖": "robot.json",
"👽": "alien.json",
"🙈": "see-no-evil-monkey.json",
"🙉": "hear-no-evil-monkey.json",
"🙊": "speak-no-evil-monkey.json",
"🌟": "glowing-star.json",
"": "sparkles.json",
"": "electricity.json",
"💥": "collision.json",
"💯": "100.json",
"🎉": "party-popper.json",
"🎊": "confetti-ball.json",
"🧡": "orange-heart.json",
"💛": "yellow-heart.json",
"💚": "green-heart.json",
"💙": "blue-heart.json",
"💜": "purple-heart.json",
"💘": "cupid.json",
"💝": "gift-heart.json",
"💖": "sparkling-heart.json",
"💕": "two-hearts.json",
"💔": "broken-heart.json",
"💋": "kiss.json",
"👀": "eyes.json",
"🦻": "hearing-aid.json",
"🦶": "foot.json",
"🦾": "arm-mechanical.json",
"👏": "clap.json",
"👍": "thumbs-up.json",
"👎": "thumbs-down.json",
"🙌": "raising-hands.json",
"": "raised-fist.json",
"👊": "fist.json",
"👋": "wave.json",
"🤘": "metal.json",
"🤞": "crossed-fingers.json",
"🤙": "call-me-hand.json",
"👌": "ok.json",
"🖕": "middle-finger.json",
"🤝": "handshake.json",
"💃": "dancer-woman.json",
"🌱": "plant.json",
"🍃": "leaves.json",
"🍀": "luck.json",
"🌊": "ocean.json",
"💧": "droplet.json",
"🦄": "unicorn.json",
"🦖": "t-rex.json",
"🦕": "dinosaur.json",
"🐢": "turtle.json",
"🐍": "snake.json",
"🐩": "poodle.json",
"🐕": "dog.json",
"🐖": "pig.json",
"🦘": "kangaroo.json",
"🦍": "gorilla.json",
"🦧": "orangutan.json",
"🦦": "otter.json",
"🐓": "rooster.json",
"🦅": "eagle.json",
"🦉": "owl.json",
"🐬": "dolphin.json",
"🐳": "whale.json",
"🐟": "fish.json",
"🐡": "blowfish.json",
"🦀": "crab.json",
"🐙": "octopus.json",
"🐌": "snail.json",
"🍻": "clinking-beer-mugs.json",
"🍾": "bottle-with-popping-cork.json",
"🚨": "police-car-light.json",
"🛸": "flying-saucer.json",
"🚀": "rocket.json",
"🛫": "airplane-departure.json",
"🎢": "roller-coaster.json",
"🎡": "ferris-wheel.json",
"🎈": "balloon.json",
"🎁": "wrapped-gift.json",
"🎆": "fireworks.json",
"💸": "money-with-wings.json",
"💎": "gem-stone.json",
"🎓": "graduation-cap.json",
"🔔": "bell.json",
"💣": "bomb.json",
"": "exclamation.json",
"": "question.json",
"": "cross-mark.json",
"🏁": "chequered-flag.json",
"🚩": "triangular-flag.json",
"🏴": "black-flag.json",
}
import os
files = [f for f in os.listdir(".") if os.path.isfile(os.path.join(".", f))]
for file in files:
if ".json" in file:
found = False
for name in emojies.values():
if name == file:
found = True
if not found:
print("DELETE", file)
os.remove(file)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,24 +1,18 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'dart:async';
import 'package:url_launcher/url_launcher.dart';
// these two callbacks are called on updated to the corresponding database
/// The Widget that configures your application.
class App extends StatefulWidget {
const App({super.key});
@override
@ -27,7 +21,6 @@ class App extends StatefulWidget {
class _AppState extends State<App> with WidgetsBindingObserver {
bool wasPaused = false;
bool appIsOutdated = false;
@override
void initState() {
@ -35,16 +28,15 @@ class _AppState extends State<App> with WidgetsBindingObserver {
globalIsAppInBackground = false;
WidgetsBinding.instance.addObserver(this);
// register global callbacks to the widget tree
globalCallbackConnectionState = (update) {
context.read<CustomChangeProvider>().updateConnectionState(update);
globalCallbackConnectionState = ({required bool isConnected}) {
context.read<CustomChangeProvider>().updateConnectionState(isConnected);
setUserPlan();
};
initAsync();
}
Future setUserPlan() async {
Future<void> setUserPlan() async {
final user = await getUser();
globalBestFriendUserId = -1;
if (user != null && mounted) {
@ -59,23 +51,19 @@ class _AppState extends State<App> with WidgetsBindingObserver {
}
}
if (mounted) {
context.read<CustomChangeProvider>().updatePlan(user.subscriptionPlan);
await context
.read<CustomChangeProvider>()
.updatePlan(user.subscriptionPlan);
}
}
}
Future initAsync() async {
setUserPlan();
globalCallbackAppIsOutdated = () async {
context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
appIsOutdated = true;
});
};
Future<void> initAsync() async {
await setUserPlan();
await apiService.connect(force: true);
apiService.listenToNetworkChanges();
await apiService.listenToNetworkChanges();
// call this function so invalid media files are get purged
retryMediaUpload(true);
await retryMediaUpload(true);
}
@override
@ -97,8 +85,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
globalCallbackConnectionState = (a) {};
globalCallbackAppIsOutdated = () {};
globalCallbackConnectionState = ({required bool isConnected}) {};
super.dispose();
}
@ -145,10 +132,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
themeMode: context.watch<SettingsChangeProvider>().themeMode,
initialRoute: '/',
routes: {
"/": (context) =>
AppMainWidget(initialPage: 1, appIsOutdated: appIsOutdated),
"/chats": (context) =>
AppMainWidget(initialPage: 0, appIsOutdated: appIsOutdated)
"/": (context) => AppMainWidget(initialPage: 1),
"/chats": (context) => AppMainWidget(initialPage: 0)
},
);
},
@ -157,17 +142,18 @@ class _AppState extends State<App> with WidgetsBindingObserver {
}
class AppMainWidget extends StatefulWidget {
const AppMainWidget(
{super.key, required this.initialPage, required this.appIsOutdated});
const AppMainWidget({
super.key,
required this.initialPage,
});
final int initialPage;
final bool appIsOutdated;
@override
State<AppMainWidget> createState() => _AppMainWidgetState();
}
class _AppMainWidgetState extends State<AppMainWidget> {
Future<bool> userCreated = isUserCreated();
bool showOnboarding = kReleaseMode;
bool showOnboarding = true;
@override
Widget build(BuildContext context) {
@ -178,80 +164,25 @@ class _AppMainWidgetState extends State<AppMainWidget> {
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: Container());
}
if (snapshot.data!) {
} else if (snapshot.data!) {
return HomeView(
initialPage: widget.initialPage,
);
} else if (showOnboarding) {
return OnboardingView(
callbackOnSuccess: () => setState(() {
showOnboarding = false;
}),
);
}
return showOnboarding
? OnboardingView(
callbackOnSuccess: () {
setState(() {
showOnboarding = false;
});
},
)
: RegisterView(
callbackOnSuccess: () {
setState(() {
userCreated = isUserCreated();
});
},
);
return RegisterView(
callbackOnSuccess: () => setState(() {
userCreated = isUserCreated();
}),
);
},
),
if (widget.appIsOutdated)
Positioned(
top: 60,
left: 30,
right: 30,
child: SafeArea(
child: Container(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 8),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
context.lang.appOutdated,
textAlign: TextAlign.center,
softWrap: true,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
if (Platform.isAndroid) SizedBox(height: 5),
if (Platform.isAndroid)
ElevatedButton(
onPressed: () {
launchUrl(Uri.parse(
"https://play.google.com/store/apps/details?id=eu.twonly"));
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
context.lang.appOutdatedBtn,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white, fontSize: 16),
),
),
],
),
),
),
),
AppOutdated(),
],
);
}

View file

@ -15,8 +15,10 @@ bool gIsDemoUser = false;
// App widget.
// This callback called by the apiProvider
Function(bool) globalCallbackConnectionState = (a) {};
Function() globalCallbackAppIsOutdated = () {};
void Function({required bool isConnected}) globalCallbackConnectionState = ({
required bool isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};
bool globalIsAppInBackground = true;
int globalBestFriendUserId = -1;

View file

@ -1,20 +1,23 @@
import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
import 'app.dart';
void main() async {
@ -31,31 +34,29 @@ void main() async {
}
final settingsController = SettingsChangeProvider();
// Load the user's preferred theme while the splash screen is displayed.
// This prevents a sudden theme change when the app is first displayed.
await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
setupPushNotification();
unawaited(setupPushNotification());
gCameras = await availableCameras();
apiService = ApiService();
twonlyDB = TwonlyDatabase();
await twonlyDB.messagesDao.resetPendingDownloadState();
await twonlyDB.messagesDao.handleMediaFilesOlderThan7Days();
await twonlyDB.signalDao.purgeOutDatedPreKeys();
// Purge media files in the background
purgeReceivedMediaFiles();
purgeSendMediaFiles();
unawaited(purgeReceivedMediaFiles());
unawaited(purgeSendMediaFiles());
performTwonlySafeBackup();
unawaited(performTwonlySafeBackup());
await initFileDownloader();
cleanLogFile();
runApp(
MultiProvider(
providers: [
@ -63,7 +64,7 @@ void main() async {
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
],
child: App(),
child: const App(),
),
);
}

View file

@ -1,11 +1,11 @@
class SecureStorageKeys {
static const String signalIdentity = "signal_identity";
static const String signalSignedPreKey = "signed_pre_key_store";
static const String apiAuthToken = "api_auth_token";
static const String googleFcm = "google_fcm";
static const String userData = "userData";
static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash";
static const String signalIdentity = 'signal_identity';
static const String signalSignedPreKey = 'signed_pre_key_store';
static const String apiAuthToken = 'api_auth_token';
static const String googleFcm = 'google_fcm';
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 = 'receiving_push_keys';
static const String sendingPushKeys = 'sending_push_keys';
}

View file

@ -20,30 +20,33 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
}
}
Future incFlameCounter(
int contactId, bool received, DateTime timestamp) async {
Contact contact = (await (select(contacts)
Future<int> incFlameCounter(
int contactId,
bool received,
DateTime timestamp,
) async {
final contact = (await (select(contacts)
..where((t) => t.userId.equals(contactId)))
.get())
.first;
int totalMediaCounter = contact.totalMediaCounter + 1;
int flameCounter = contact.flameCounter;
final totalMediaCounter = contact.totalMediaCounter + 1;
var flameCounter = contact.flameCounter;
if (contact.lastMessageReceived != null &&
contact.lastMessageSend != null) {
final now = DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final twoDaysAgo = startOfToday.subtract(Duration(days: 2));
final twoDaysAgo = startOfToday.subtract(const Duration(days: 2));
if (contact.lastMessageSend!.isBefore(twoDaysAgo) ||
contact.lastMessageReceived!.isBefore(twoDaysAgo)) {
flameCounter = 0;
}
}
Value<DateTime?> lastMessageSend = Value.absent();
Value<DateTime?> lastMessageReceived = Value.absent();
Value<DateTime?> lastFlameCounterChange = Value.absent();
var lastMessageSend = const Value<DateTime?>.absent();
var lastMessageReceived = const Value<DateTime?>.absent();
var lastFlameCounterChange = const Value<DateTime?>.absent();
if (contact.lastFlameCounterChange != null) {
final now = DateTime.now();
@ -51,7 +54,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
if (contact.lastFlameCounterChange!.isBefore(startOfToday)) {
// last flame update was yesterday. check if it can be updated.
bool updateFlame = false;
var updateFlame = false;
if (received) {
if (contact.lastMessageSend != null &&
contact.lastMessageSend!.isAfter(startOfToday)) {
@ -94,11 +97,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
return select(contacts)..where((t) => t.userId.equals(userId));
}
Future deleteContactByUserId(int userId) {
Future<void> deleteContactByUserId(int userId) {
return (delete(contacts)..where((t) => t.userId.equals(userId))).go();
}
Future updateContact(int userId, ContactsCompanion updatedValues) async {
Future<void> updateContact(
int userId, ContactsCompanion updatedValues) async {
await ((update(contacts)..where((c) => c.userId.equals(userId)))
.write(updatedValues));
if (updatedValues.blocked.present ||
@ -111,7 +115,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
}
}
Future newMessageExchange(int userId) {
Future<void> newMessageExchange(int userId) {
return updateContact(
userId,
ContactsCompanion(

View file

@ -11,7 +11,7 @@ class MediaDownloadsDao extends DatabaseAccessor<TwonlyDatabase>
with _$MediaDownloadsDaoMixin {
MediaDownloadsDao(super.db);
Future updateMediaDownload(
Future<void> updateMediaDownload(
int messageId, MediaDownloadsCompanion updatedValues) {
return (update(mediaDownloads)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues);
@ -26,7 +26,7 @@ class MediaDownloadsDao extends DatabaseAccessor<TwonlyDatabase>
}
}
Future deleteMediaDownload(int messageId) {
Future<void> deleteMediaDownload(int messageId) {
return (delete(mediaDownloads)..where((t) => t.messageId.equals(messageId)))
.go();
}

View file

@ -33,7 +33,7 @@ class MediaUploadsDao extends DatabaseAccessor<TwonlyDatabase>
}
}
Future deleteMediaUpload(int mediaUploadId) {
Future<void> deleteMediaUpload(int mediaUploadId) {
return (delete(mediaUploads)
..where((t) => t.mediaUploadId.equals(mediaUploadId)))
.go();

View file

@ -45,7 +45,7 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
..where((t) => t.retransmissionId.equals(retransmissionId));
}
Future updateRetransmission(
Future<void> updateRetransmission(
int retransmissionId,
MessageRetransmissionsCompanion updatedValues,
) {
@ -54,13 +54,13 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
.write(updatedValues);
}
Future resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async {
Future<int> resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async {
return ((update(messageRetransmissions))
..where((m) =>
m.contactId.equals(fromUserId) &
m.encryptedHash.equals(encryptedHash)))
.write(
MessageRetransmissionsCompanion(
const MessageRetransmissionsCompanion(
acknowledgeByServerAt: Value(null),
),
);
@ -75,13 +75,13 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
.getSingleOrNull();
}
Future deleteRetransmissionById(int retransmissionId) {
Future<void> deleteRetransmissionById(int retransmissionId) {
return (delete(messageRetransmissions)
..where((t) => t.retransmissionId.equals(retransmissionId)))
.go();
}
Future deleteRetransmissionByMessageId(int messageId) {
Future<void> deleteRetransmissionByMessageId(int messageId) {
return (delete(messageRetransmissions)
..where((t) => t.messageId.equals(messageId)))
.go();

View file

@ -56,7 +56,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.watch();
}
Future removeOldMessages() {
Future<void> removeOldMessages() {
return (update(messages)
..where((t) =>
(t.openedAt.isSmallerThanValue(
@ -69,7 +69,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(MessagesCompanion(contentJson: Value(null)));
}
Future handleMediaFilesOlderThan7Days() {
Future<void> handleMediaFilesOlderThan7Days() {
/// media files will be deleted by the server after 7 days, so delete them here also
return (update(messages)
..where(
@ -130,7 +130,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.get();
}
Future openedAllNonMediaMessages(int contactId) {
Future<void> openedAllNonMediaMessages(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages)
..where((t) =>
@ -141,7 +141,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updates);
}
Future resetPendingDownloadState() {
Future<void> resetPendingDownloadState() {
// All media files in the downloading state are reseteded 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...
@ -155,7 +155,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updates);
}
Future openedAllNonMediaMessagesFromOtherUser(int contactId) {
Future<void> openedAllNonMediaMessagesFromOtherUser(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages)
..where((t) =>
@ -167,7 +167,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updates);
}
Future updateMessageByOtherUser(
Future<void> updateMessageByOtherUser(
int userId, int messageId, MessagesCompanion updatedValues) {
return (update(messages)
..where((c) =>
@ -175,7 +175,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updatedValues);
}
Future updateMessageByOtherMessageId(
Future<void> updateMessageByOtherMessageId(
int userId, int messageOtherId, MessagesCompanion updatedValues) {
return (update(messages)
..where((c) =>
@ -184,7 +184,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(updatedValues);
}
Future updateMessageByMessageId(
Future<void> updateMessageByMessageId(
int messageId, MessagesCompanion updatedValues) {
return (update(messages)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues);
@ -203,14 +203,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
}
}
Future deleteMessagesByContactId(int contactId) {
Future<void> deleteMessagesByContactId(int contactId) {
return (delete(messages)
..where((t) =>
t.contactId.equals(contactId) & t.mediaStored.equals(false)))
.go();
}
Future deleteMessagesByContactIdAndOtherMessageId(
Future<void> deleteMessagesByContactIdAndOtherMessageId(
int contactId, int messageOtherId) {
return (delete(messages)
..where((t) =>
@ -219,11 +219,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.go();
}
Future deleteMessagesByMessageId(int messageId) {
Future<void> deleteMessagesByMessageId(int messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
}
Future deleteAllMessagesByContactId(int contactId) {
Future<void> deleteAllMessagesByContactId(int contactId) {
return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();
}

View file

@ -15,7 +15,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
SignalDao(super.db);
Future deleteAllByContactId(int contactId) async {
Future<void> deleteAllByContactId(int contactId) async {
await (delete(signalContactPreKeys)
..where((t) => t.contactId.equals(contactId)))
.go();
@ -24,7 +24,7 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
.go();
}
Future deleteAllPreKeysByContactId(int contactId) async {
Future<void> deleteAllPreKeysByContactId(int contactId) async {
await (delete(signalContactPreKeys)
..where((t) => t.contactId.equals(contactId)))
.go();

View file

@ -1,7 +1,7 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
class ConnectIdentityKeyStore extends IdentityKeyStore {
@ -12,13 +12,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
@override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
SignalIdentityKeyStore? identity =
await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
..where((t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName())))
.getSingleOrNull();
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
..where((t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName())))
.getSingleOrNull();
if (identity == null) return null;
return IdentityKey.fromBytes(identity.identityKey, 0);
}
@ -37,7 +35,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
return false;
}
return trusted == null ||
ListEquality().equals(trusted.serialize(), identityKey.serialize());
const ListEquality<dynamic>()
.equals(trusted.serialize(), identityKey.serialize());
}
@override

View file

@ -1,6 +1,6 @@
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart';
@ -21,7 +21,7 @@ class ConnectPreKeyStore extends PreKeyStore {
if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException('No such preKey record! - $preKeyId');
}
Uint8List preKey = preKeyRecord.first.preKey;
final preKey = preKeyRecord.first.preKey;
return PreKeyRecord.fromBuffer(preKey);
}
@ -42,7 +42,7 @@ class ConnectPreKeyStore extends PreKeyStore {
try {
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
} catch (e) {
Log.error("$e");
Log.error('$e');
}
}
}

View file

@ -7,24 +7,24 @@ import 'package:twonly/src/constants/secure_storage_keys.dart';
class ConnectSignedPreKeyStore extends SignedPreKeyStore {
Future<HashMap<int, Uint8List>> getStore() async {
final storage = FlutterSecureStorage();
const storage = FlutterSecureStorage();
final storeSerialized = await storage.read(
key: SecureStorageKeys.signalSignedPreKey,
);
var store = HashMap<int, Uint8List>();
final store = HashMap<int, Uint8List>();
if (storeSerialized == null) {
return store;
}
final storeHashMap = json.decode(storeSerialized);
final storeHashMap = json.decode(storeSerialized) as List<List<dynamic>>;
for (final item in storeHashMap) {
store[item[0]] = base64Decode(item[1]);
store[item[0] as int] = base64Decode(item[1] as String);
}
return store;
}
Future safeStore(HashMap<int, Uint8List> store) async {
final storage = FlutterSecureStorage();
var storeHashMap = [];
Future<void> safeStore(HashMap<int, Uint8List> store) async {
const storage = FlutterSecureStorage();
final storeHashMap = <List<dynamic>>[];
for (final item in store.entries) {
storeHashMap.add([item.key, base64Encode(item.value)]);
}

View file

@ -6,7 +6,6 @@ enum UploadState {
readyToUpload,
uploadTaskStarted,
receiverNotified,
// after all users notified all media files that are not storable by the other person will be deleted
}
@DataClassName('MediaUpload')
@ -16,18 +15,30 @@ class MediaUploads extends Table {
textEnum<UploadState>().withDefault(Constant(UploadState.pending.name))();
TextColumn get metadata =>
text().map(MediaUploadMetadataConverter()).nullable()();
text().map(const MediaUploadMetadataConverter()).nullable()();
/// exists in UploadState.addedToMessagesDb
TextColumn get messageIds => text().map(IntListTypeConverter()).nullable()();
TextColumn get encryptionData =>
text().map(MediaEncryptionDataConverter()).nullable()();
text().map(const MediaEncryptionDataConverter()).nullable()();
}
// --- state ----
class MediaUploadMetadata {
MediaUploadMetadata();
factory MediaUploadMetadata.fromJson(Map<String, dynamic> json) {
return MediaUploadMetadata()
..contactIds = List<int>.from(json['contactIds'] as Iterable<dynamic>)
..isRealTwonly = json['isRealTwonly'] as bool
..isVideo = json['isVideo'] as bool
..mirrorVideo = json['mirrorVideo'] as bool
..maxShowTime = json['maxShowTime'] as int
..maxShowTime = json['maxShowTime'] as int
..messageSendAt = DateTime.parse(json['messageSendAt'] as String);
}
late List<int> contactIds;
late bool isRealTwonly;
late int maxShowTime;
@ -35,8 +46,6 @@ class MediaUploadMetadata {
late bool isVideo;
late bool mirrorVideo;
MediaUploadMetadata();
Map<String, dynamic> toJson() {
return {
'contactIds': contactIds,
@ -47,28 +56,26 @@ class MediaUploadMetadata {
'messageSendAt': messageSendAt.toIso8601String(),
};
}
factory MediaUploadMetadata.fromJson(Map<String, dynamic> json) {
MediaUploadMetadata state = MediaUploadMetadata();
state.contactIds = List<int>.from(json['contactIds']);
state.isRealTwonly = json['isRealTwonly'];
state.isVideo = json['isVideo'];
state.mirrorVideo = json['mirrorVideo'];
state.maxShowTime = json['maxShowTime'];
state.maxShowTime = json['maxShowTime'];
state.messageSendAt = DateTime.parse(json['messageSendAt']);
return state;
}
}
class MediaEncryptionData {
MediaEncryptionData();
factory MediaEncryptionData.fromJson(Map<String, dynamic> json) {
return MediaEncryptionData()
..sha2Hash = List<int>.from(json['sha2Hash'] as Iterable<dynamic>)
..encryptionKey =
List<int>.from(json['encryptionKey'] as Iterable<dynamic>)
..encryptionMac =
List<int>.from(json['encryptionMac'] as Iterable<dynamic>)
..encryptionNonce =
List<int>.from(json['encryptionNonce'] as Iterable<dynamic>);
}
late List<int> sha2Hash;
late List<int> encryptionKey;
late List<int> encryptionMac;
late List<int> encryptionNonce;
MediaEncryptionData();
Map<String, dynamic> toJson() {
return {
'sha2Hash': sha2Hash,
@ -77,15 +84,6 @@ class MediaEncryptionData {
'encryptionNonce': encryptionNonce,
};
}
factory MediaEncryptionData.fromJson(Map<String, dynamic> json) {
MediaEncryptionData state = MediaEncryptionData();
state.sha2Hash = List<int>.from(json['sha2Hash']);
state.encryptionKey = List<int>.from(json['encryptionKey']);
state.encryptionMac = List<int>.from(json['encryptionMac']);
state.encryptionNonce = List<int>.from(json['encryptionNonce']);
return state;
}
}
// --- converters ----
@ -93,7 +91,7 @@ class MediaEncryptionData {
class IntListTypeConverter extends TypeConverter<List<int>, String> {
@override
List<int> fromSql(String fromDb) {
return List<int>.from(jsonDecode(fromDb));
return List<int>.from(jsonDecode(fromDb) as Iterable<dynamic>);
}
@override

View file

@ -1,6 +1,6 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'
show driftDatabase, DriftNativeOptions;
show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/daos/media_downloads_dao.dart';
@ -73,15 +73,15 @@ class TwonlyDatabase extends _$TwonlyDatabase {
},
onUpgrade: stepByStep(
from1To2: (m, schema) async {
m.addColumn(schema.messages, schema.messages.errorWhileSending);
await m.addColumn(schema.messages, schema.messages.errorWhileSending);
},
from2To3: (m, schema) async {
m.addColumn(schema.contacts, schema.contacts.archived);
m.addColumn(
await m.addColumn(schema.contacts, schema.contacts.archived);
await m.addColumn(
schema.contacts, schema.contacts.deleteMessagesAfterXMinutes);
},
from3To4: (m, schema) async {
m.createTable(schema.mediaUploads);
await m.createTable(schema.mediaUploads);
await m.alterTable(TableMigration(
schema.mediaUploads,
columnTransformer: {
@ -91,19 +91,19 @@ class TwonlyDatabase extends _$TwonlyDatabase {
));
},
from4To5: (m, schema) async {
m.createTable(mediaDownloads);
m.addColumn(schema.messages, schema.messages.mediaDownloadId);
m.addColumn(schema.messages, schema.messages.mediaUploadId);
await m.createTable(mediaDownloads);
await m.addColumn(schema.messages, schema.messages.mediaDownloadId);
await m.addColumn(schema.messages, schema.messages.mediaUploadId);
},
from5To6: (m, schema) async {
m.addColumn(schema.messages, schema.messages.mediaStored);
await m.addColumn(schema.messages, schema.messages.mediaStored);
},
from6To7: (m, schema) async {
m.addColumn(schema.contacts, schema.contacts.pinned);
await m.addColumn(schema.contacts, schema.contacts.pinned);
},
from7To8: (m, schema) async {
m.addColumn(schema.contacts, schema.contacts.alsoBestFriend);
m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
await m.addColumn(schema.contacts, schema.contacts.alsoBestFriend);
await m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
},
from8To9: (m, schema) async {
await m.alterTable(TableMigration(
@ -115,29 +115,29 @@ class TwonlyDatabase extends _$TwonlyDatabase {
));
},
from9To10: (m, schema) async {
m.createTable(schema.signalContactPreKeys);
m.createTable(schema.signalContactSignedPreKeys);
m.addColumn(schema.contacts, schema.contacts.deleted);
await m.createTable(schema.signalContactPreKeys);
await m.createTable(schema.signalContactSignedPreKeys);
await m.addColumn(schema.contacts, schema.contacts.deleted);
},
from10To11: (m, schema) async {
m.createTable(schema.messageRetransmissions);
await m.createTable(schema.messageRetransmissions);
},
from11To12: (m, schema) async {
m.addColumn(schema.messageRetransmissions,
await m.addColumn(schema.messageRetransmissions,
schema.messageRetransmissions.willNotGetACKByUser);
},
from12To13: (m, schema) async {
m.dropColumn(
schema.messageRetransmissions, "will_not_get_a_c_k_by_user");
await m.dropColumn(
schema.messageRetransmissions, 'will_not_get_a_c_k_by_user');
},
from13To14: (m, schema) async {
m.addColumn(schema.messageRetransmissions,
await m.addColumn(schema.messageRetransmissions,
schema.messageRetransmissions.encryptedHash);
},
from14To15: (m, schema) async {
m.dropColumn(schema.mediaUploads, "upload_tokens");
m.dropColumn(schema.mediaUploads, "already_notified");
m.addColumn(
await m.dropColumn(schema.mediaUploads, 'upload_tokens');
await m.dropColumn(schema.mediaUploads, 'already_notified');
await m.addColumn(
schema.messages, schema.messages.mediaRetransmissionState);
},
),
@ -161,13 +161,13 @@ class TwonlyDatabase extends _$TwonlyDatabase {
}
}
Future deleteDataForTwonlySafe() async {
Future<void> deleteDataForTwonlySafe() async {
await delete(messages).go();
await delete(messageRetransmissions).go();
await delete(mediaDownloads).go();
await delete(mediaUploads).go();
await update(contacts).write(
ContactsCompanion(
const ContactsCompanion(
avatarSvg: Value(null),
myAvatarCounter: Value(0),
),
@ -177,7 +177,7 @@ class TwonlyDatabase extends _$TwonlyDatabase {
await (delete(signalPreKeyStores)
..where((t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract(
Duration(days: 25),
const Duration(days: 25),
),
))))
.go();

View file

@ -89,7 +89,7 @@
"messageSendState_Sending": "Wird gesendet",
"messageSendState_TapToLoad": "Tippe zum Laden",
"messageSendState_Loading": "Herunterladen",
"messageStoredInGalery": "Gespeichert",
"messageStoredInGallery": "Gespeichert",
"messageReopened": "Erneut geöffnet",
"@messageReopened": {},
"imageEditorDrawOk": "Zeichnung machen",
@ -329,7 +329,7 @@
"appOutdatedBtn": "Jetzt aktualisieren.",
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
"retransmissionRequested": "Wird erneut versucht.",
"testPaymentMethode": "twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!",
"testPaymentMethod": "twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!",
"testingAccountTitle": "Tester-Zugang",
"testingAccountBody": "Danke für dein Interesse! Wir werden deine Anfrage prüfen und den Plan so schnell wie möglich aktivieren. Da wir uns jedoch noch in einer Testphase befinden, ist die Anzahl der Tester-Konten begrenzt. Wir werden dich jedoch benachrichtigen, sobald dir ein Platz zugewiesen wurde."
}

View file

@ -145,8 +145,8 @@
"@messageSendState_TapToLoad": {},
"messageSendState_Loading": "Downloading",
"@messageSendState_Loading": {},
"messageStoredInGalery": "Stored in gallery",
"@messageStoredInGalery": {},
"messageStoredInGallery": "Stored in gallery",
"@messageStoredInGallery": {},
"messageReopened": "Re-opened",
"@messageReopened": {},
"imageEditorDrawOk": "Take drawing",
@ -486,7 +486,7 @@
"appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again",
"retransmissionRequested": "Retransmission requested",
"testPaymentMethode": "twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!",
"testPaymentMethod": "twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!",
"testingAccountTitle": "Tester account activation",
"testingAccountBody": "Thank you for your interest! We will check your request and activate the plan as soon as possible. However, as we are currently still in a test phase, the number of tester accounts is limited. However, we will notify you as soon as you have been allocated a place."
}

View file

@ -542,11 +542,11 @@ abstract class AppLocalizations {
/// **'Downloading'**
String get messageSendState_Loading;
/// No description provided for @messageStoredInGalery.
/// No description provided for @messageStoredInGallery.
///
/// In en, this message translates to:
/// **'Stored in gallery'**
String get messageStoredInGalery;
String get messageStoredInGallery;
/// No description provided for @messageReopened.
///
@ -2012,11 +2012,11 @@ abstract class AppLocalizations {
/// **'Retransmission requested'**
String get retransmissionRequested;
/// No description provided for @testPaymentMethode.
/// No description provided for @testPaymentMethod.
///
/// In en, this message translates to:
/// **'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!'**
String get testPaymentMethode;
String get testPaymentMethod;
/// No description provided for @testingAccountTitle.
///

View file

@ -253,7 +253,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get messageSendState_Loading => 'Herunterladen';
@override
String get messageStoredInGalery => 'Gespeichert';
String get messageStoredInGallery => 'Gespeichert';
@override
String get messageReopened => 'Erneut geöffnet';
@ -1068,7 +1068,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get retransmissionRequested => 'Wird erneut versucht.';
@override
String get testPaymentMethode =>
String get testPaymentMethod =>
'twonly befindet sich derzeit in einer Testphase und kann nur mit einem Einladungscode vollständig genutzt werden. Es gibt derzeit keine Zahlungsmethode, um Ihr twonly-Guthaben aufzuladen!';
@override

View file

@ -250,7 +250,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get messageSendState_Loading => 'Downloading';
@override
String get messageStoredInGalery => 'Stored in gallery';
String get messageStoredInGallery => 'Stored in gallery';
@override
String get messageReopened => 'Re-opened';
@ -1062,7 +1062,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get retransmissionRequested => 'Retransmission requested';
@override
String get testPaymentMethode =>
String get testPaymentMethod =>
'twonly is currently in a test phase and can only be used in full with an invitation code. There is currently no payment method to top up your twonly credit!';
@override

View file

@ -1,3 +1,5 @@
// ignore_for_file: strict_raw_type, prefer_constructors_over_static_methods
import 'package:flutter/material.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/utils/misc.dart';
@ -34,6 +36,14 @@ extension MessageKindExtension on MessageKind {
}
class MessageJson {
MessageJson({
required this.kind,
required this.content,
required this.timestamp,
this.messageReceiverId,
this.messageSenderId,
this.retransId,
});
final MessageKind kind;
final MessageContent? content;
final int? messageReceiverId;
@ -41,22 +51,13 @@ class MessageJson {
int? retransId;
DateTime timestamp;
MessageJson({
required this.kind,
this.messageReceiverId,
this.messageSenderId,
this.retransId,
required this.content,
required this.timestamp,
});
@override
String toString() {
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
}
static MessageJson fromJson(Map<String, dynamic> json) {
final kind = MessageKindExtension.fromString(json["kind"]);
final kind = MessageKindExtension.fromString(json['kind'] as String);
return MessageJson(
kind: kind,
@ -65,7 +66,7 @@ class MessageJson {
retransId: (json['retransId'] as num?)?.toInt(),
content: MessageContent.fromJson(
kind, json['content'] as Map<String, dynamic>),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp']),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
);
}
@ -100,9 +101,15 @@ class MessageContent {
return AckContent.fromJson(json);
case MessageKind.signalDecryptError:
return SignalDecryptErrorContent.fromJson(json);
default:
return null;
case MessageKind.storedMediaFile:
case MessageKind.contactRequest:
case MessageKind.rejectRequest:
case MessageKind.acceptRequest:
case MessageKind.opened:
case MessageKind.requestPushKey:
case MessageKind.receiveMediaError:
}
return null;
}
Map toJson() {
@ -111,15 +118,6 @@ class MessageContent {
}
class MediaMessageContent extends MessageContent {
final int maxShowTime;
final bool isRealTwonly;
final bool isVideo;
final bool mirrorVideo;
final List<int>? downloadToken;
final List<int>? encryptionKey;
final List<int>? encryptionMac;
final List<int>? encryptionNonce;
MediaMessageContent({
required this.maxShowTime,
required this.isRealTwonly,
@ -130,25 +128,33 @@ class MediaMessageContent extends MessageContent {
this.encryptionMac,
this.encryptionNonce,
});
final int maxShowTime;
final bool isRealTwonly;
final bool isVideo;
final bool mirrorVideo;
final List<int>? downloadToken;
final List<int>? encryptionKey;
final List<int>? encryptionMac;
final List<int>? encryptionNonce;
static MediaMessageContent fromJson(Map json) {
return MediaMessageContent(
downloadToken: json['downloadToken'] == null
? null
: List<int>.from(json['downloadToken']),
: List<int>.from(json['downloadToken'] as List),
encryptionKey: json['encryptionKey'] == null
? null
: List<int>.from(json['encryptionKey']),
: List<int>.from(json['encryptionKey'] as List),
encryptionMac: json['encryptionMac'] == null
? null
: List<int>.from(json['encryptionMac']),
: List<int>.from(json['encryptionMac'] as List),
encryptionNonce: json['encryptionNonce'] == null
? null
: List<int>.from(json['encryptionNonce']),
maxShowTime: json['maxShowTime'],
isRealTwonly: json['isRealTwonly'],
isVideo: json['isVideo'] ?? false,
mirrorVideo: json['mirrorVideo'] ?? false,
: List<int>.from(json['encryptionNonce'] as List),
maxShowTime: json['maxShowTime'] as int,
isRealTwonly: json['isRealTwonly'] as bool,
isVideo: json['isVideo'] as bool? ?? false,
mirrorVideo: json['mirrorVideo'] as bool? ?? false,
);
}
@ -168,23 +174,23 @@ class MediaMessageContent extends MessageContent {
}
class TextMessageContent extends MessageContent {
String text;
int? responseToMessageId;
int? responseToOtherMessageId;
TextMessageContent({
required this.text,
this.responseToMessageId,
this.responseToOtherMessageId,
});
String text;
int? responseToMessageId;
int? responseToOtherMessageId;
static TextMessageContent fromJson(Map json) {
return TextMessageContent(
text: json['text'],
text: json['text'] as String,
responseToOtherMessageId: json.containsKey('responseToOtherMessageId')
? json['responseToOtherMessageId']
? json['responseToOtherMessageId'] as int?
: null,
responseToMessageId: json.containsKey('responseToMessageId')
? json['responseToMessageId']
? json['responseToMessageId'] as int?
: null);
}
@ -199,11 +205,11 @@ class TextMessageContent extends MessageContent {
}
class ReopenedMediaFileContent extends MessageContent {
int messageId;
ReopenedMediaFileContent({required this.messageId});
int messageId;
static ReopenedMediaFileContent fromJson(Map json) {
return ReopenedMediaFileContent(messageId: json['messageId']);
return ReopenedMediaFileContent(messageId: json['messageId'] as int);
}
@override
@ -213,12 +219,12 @@ class ReopenedMediaFileContent extends MessageContent {
}
class SignalDecryptErrorContent extends MessageContent {
List<int> encryptedHash;
SignalDecryptErrorContent({required this.encryptedHash});
List<int> encryptedHash;
static SignalDecryptErrorContent fromJson(Map json) {
return SignalDecryptErrorContent(
encryptedHash: List<int>.from(json['encryptedHash']),
encryptedHash: List<int>.from(json['encryptedHash'] as List),
);
}
@ -231,14 +237,14 @@ class SignalDecryptErrorContent extends MessageContent {
}
class AckContent extends MessageContent {
AckContent({required this.messageIdToAck, required this.retransIdToAck});
int? messageIdToAck;
int retransIdToAck;
AckContent({required this.messageIdToAck, required this.retransIdToAck});
static AckContent fromJson(Map json) {
return AckContent(
messageIdToAck: json['messageIdToAck'],
retransIdToAck: json['retransIdToAck'],
messageIdToAck: json['messageIdToAck'] as int,
retransIdToAck: json['retransIdToAck'] as int,
);
}
@ -252,14 +258,14 @@ class AckContent extends MessageContent {
}
class ProfileContent extends MessageContent {
ProfileContent({required this.avatarSvg, required this.displayName});
String avatarSvg;
String displayName;
ProfileContent({required this.avatarSvg, required this.displayName});
static ProfileContent fromJson(Map json) {
return ProfileContent(
avatarSvg: json['avatarSvg'],
displayName: json['displayName'],
avatarSvg: json['avatarSvg'] as String,
displayName: json['displayName'] as String,
);
}
@ -270,14 +276,14 @@ class ProfileContent extends MessageContent {
}
class PushKeyContent extends MessageContent {
PushKeyContent({required this.keyId, required this.key});
int keyId;
List<int> key;
PushKeyContent({required this.keyId, required this.key});
static PushKeyContent fromJson(Map json) {
return PushKeyContent(
keyId: json['keyId'],
key: List<int>.from(json['key']),
keyId: json['keyId'] as int,
key: List<int>.from(json['key'] as List),
);
}
@ -291,21 +297,20 @@ class PushKeyContent extends MessageContent {
}
class FlameSyncContent extends MessageContent {
int flameCounter;
DateTime lastFlameCounterChange;
bool bestFriend;
FlameSyncContent(
{required this.flameCounter,
required this.bestFriend,
required this.lastFlameCounterChange});
int flameCounter;
DateTime lastFlameCounterChange;
bool bestFriend;
static FlameSyncContent fromJson(Map json) {
return FlameSyncContent(
flameCounter: json['flameCounter'],
bestFriend: json['bestFriend'],
lastFlameCounterChange:
DateTime.fromMillisecondsSinceEpoch(json['lastFlameCounterChange']),
flameCounter: json['flameCounter'] as int,
bestFriend: json['bestFriend'] as bool,
lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch(
json['lastFlameCounterChange'] as int),
);
}

View file

@ -1,10 +1,11 @@
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/media_upload.dart' as send;
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/media_upload.dart' as send;
import 'package:twonly/src/services/thumbnail.service.dart';
class MemoryItem {
@ -28,45 +29,45 @@ class MemoryItem {
static Future<Map<int, MemoryItem>> convertFromMessages(
List<Message> messages,
) async {
Map<int, MemoryItem> items = {};
final items = <int, MemoryItem>{};
for (final message in messages) {
bool isSend = message.messageOtherId == null;
int id = message.mediaUploadId ?? message.messageId;
final isSend = message.messageOtherId == null;
final id = message.mediaUploadId ?? message.messageId;
final basePath = await send.getMediaFilePath(
isSend ? message.mediaUploadId! : message.messageId,
isSend ? "send" : "received",
isSend ? 'send' : 'received',
);
File? imagePath;
late File thumbnailFile;
File? videoPath;
if (await File("$basePath.mp4").exists()) {
videoPath = File("$basePath.mp4");
if (File('$basePath.mp4').existsSync()) {
videoPath = File('$basePath.mp4');
thumbnailFile = getThumbnailPath(videoPath);
if (!await thumbnailFile.exists()) {
if (!thumbnailFile.existsSync()) {
await createThumbnailsForVideo(videoPath);
}
} else if (await File("$basePath.png").exists()) {
imagePath = File("$basePath.png");
} else if (File('$basePath.png').existsSync()) {
imagePath = File('$basePath.png');
thumbnailFile = getThumbnailPath(imagePath);
if (!await thumbnailFile.exists()) {
if (!thumbnailFile.existsSync()) {
await createThumbnailsForImage(imagePath);
}
} else {
if (message.mediaStored) {
/// media file was deleted, ... remove the file
twonlyDB.messagesDao.updateMessageByMessageId(
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(
const MessagesCompanion(
mediaStored: Value(false),
),
);
}
continue;
}
bool mirrorVideo = false;
var mirrorVideo = false;
if (videoPath != null) {
MediaMessageContent content =
MediaMessageContent.fromJson(jsonDecode(message.contentJson!));
final content = MediaMessageContent.fromJson(
jsonDecode(message.contentJson!) as Map);
mirrorVideo = content.mirrorVideo;
}

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,18 @@
// ignore_for_file: avoid_dynamic_calls, strict_raw_type
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:mutex/mutex.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart';
@ -18,24 +23,22 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/server_messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/prekeys.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:web_socket_channel/io.dart';
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
final lockConnecting = Mutex();
@ -45,12 +48,12 @@ final lockRetransStore = Mutex();
/// It handles errors and does automatically tries to reconnect on
/// errors or network changes.
class ApiService {
final String apiHost = (kDebugMode) ? "10.99.0.140:3030" : "api.twonly.eu";
final String apiSecure = (kDebugMode) ? "" : "s";
ApiService();
final String apiHost = kDebugMode ? '10.99.0.140:3030' : 'api.twonly.eu';
final String apiSecure = kDebugMode ? '' : 's';
bool appIsOutdated = false;
bool isAuthenticated = false;
ApiService();
// reconnection params
Timer? reconnectionTimer;
@ -69,51 +72,51 @@ class ApiService {
_channel = channel;
_channel!.stream.listen(_onData, onDone: _onDone, onError: _onError);
await _channel!.ready;
Log.info("websocket connected to $apiUrl");
Log.info('websocket connected to $apiUrl');
return true;
} on WebSocketChannelException catch (e) {
if (!e.message
.toString()
.contains("No address associated with hostname")) {
Log.error("could not connect to api got: $e");
.contains('No address associated with hostname')) {
Log.error('could not connect to api got: $e');
}
return false;
}
}
// Function is called after the user is authenticated at the server
Future onAuthenticated() async {
Future<void> onAuthenticated() async {
isAuthenticated = true;
initFCMAfterAuthenticated();
globalCallbackConnectionState(true);
await initFCMAfterAuthenticated();
globalCallbackConnectionState(isConnected: true);
if (!globalIsAppInBackground) {
retransmitRawBytes();
tryTransmitMessages();
retryMediaUpload(false);
tryDownloadAllMediaFiles();
notifyContactsAboutProfileChange();
unawaited(retransmitRawBytes());
unawaited(tryTransmitMessages());
unawaited(retryMediaUpload(false));
unawaited(tryDownloadAllMediaFiles());
unawaited(notifyContactsAboutProfileChange());
twonlyDB.markUpdated();
syncFlameCounters();
setupNotificationWithUsers();
signalHandleNewServerConnection();
unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers());
unawaited(signalHandleNewServerConnection());
}
}
Future onConnected() async {
Future<void> onConnected() async {
await authenticate();
_reconnectionDelay = 5;
globalCallbackConnectionState(true);
globalCallbackConnectionState(isConnected: true);
}
Future onClosed() async {
Future<void> onClosed() async {
_channel = null;
isAuthenticated = false;
globalCallbackConnectionState(false);
globalCallbackConnectionState(isConnected: false);
await twonlyDB.messagesDao.resetPendingDownloadState();
}
Future startReconnectionTimer() async {
Future<void> startReconnectionTimer() async {
reconnectionTimer?.cancel();
reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () {
reconnectionTimer = null;
@ -122,18 +125,18 @@ class ApiService {
_reconnectionDelay += 5;
}
Future close(Function callback) async {
Log.info("closing websocket connection");
Future<void> close(Function callback) async {
Log.info('closing websocket connection');
if (_channel != null) {
await _channel!.sink.close();
onClosed();
await onClosed();
callback();
return;
}
callback();
}
Future listenToNetworkChanges() async {
Future<void> listenToNetworkChanges() async {
if (connectivitySubscription != null) {
return;
}
@ -155,7 +158,7 @@ class ApiService {
reconnectionTimer = null;
final user = await getUser();
if (user != null && user.isDemoUser) {
globalCallbackConnectionState(true);
globalCallbackConnectionState(isConnected: true);
return false;
}
return lockConnecting.protect<bool>(() async {
@ -168,9 +171,9 @@ class ApiService {
isAuthenticated = false;
String apiUrl = "ws$apiSecure://$apiHost/api/client";
final apiUrl = 'ws$apiSecure://$apiHost/api/client';
Log.info("connecting to $apiUrl");
Log.info('connecting to $apiUrl');
if (await _connectTo(apiUrl)) {
await onConnected();
@ -183,18 +186,18 @@ class ApiService {
bool get isConnected => _channel != null && _channel!.closeCode != null;
void _onDone() {
Log.info("websocket closed without error");
Log.info('websocket closed without error');
onClosed();
}
void _onError(dynamic e) {
Log.error("websocket error: $e");
Log.error('websocket error: $e');
onClosed();
}
void _onData(dynamic msgBuffer) async {
try {
final msg = server.ServerToClient.fromBuffer(msgBuffer);
final msg = server.ServerToClient.fromBuffer(msgBuffer as Uint8List);
if (msg.v0.hasResponse()) {
removeFromRetransmissionBuffer(msg.v0.seq);
messagesV0[msg.v0.seq] = msg;
@ -202,7 +205,7 @@ class ApiService {
await handleServerMessage(msg);
}
} catch (e) {
Log.error("Error parsing the servers message: $e");
Log.error('Error parsing the servers message: $e');
}
}
@ -218,57 +221,57 @@ class ApiService {
return tmp;
}
if (DateTime.now().difference(startTime) > timeout) {
Log.error("Timeout for message $seq");
Log.error('Timeout for message $seq');
return null;
}
await Future.delayed(Duration(milliseconds: 10));
await Future.delayed(const Duration(milliseconds: 10));
}
}
Future sendResponse(ClientToServer response) async {
Future<void> sendResponse(ClientToServer response) async {
if (_channel != null) {
_channel!.sink.add(response.writeToBuffer());
}
}
Future<Map<String, dynamic>> getRetransmission() async {
return await KeyValueStore.get("rawbytes-to-retransmit") ?? {};
return (await KeyValueStore.get('rawbytes-to-retransmit')) ?? {};
}
Future retransmitRawBytes() async {
Future<void> retransmitRawBytes() async {
await lockRetransStore.protect(() async {
var retransmit = await getRetransmission();
final retransmit = await getRetransmission();
if (retransmit.keys.isEmpty) return;
Log.info("retransmitting ${retransmit.keys.length} raw bytes messages");
bool gotError = false;
Log.info('retransmitting ${retransmit.keys.length} raw bytes messages');
var gotError = false;
for (final seq in retransmit.keys) {
try {
_channel!.sink.add(base64Decode(retransmit[seq]));
_channel!.sink.add(base64Decode(retransmit[seq] as String));
} catch (e) {
gotError = true;
Log.error("$e");
Log.error('$e');
}
}
if (!gotError) {
KeyValueStore.put("rawbytes-to-retransmit", {});
await KeyValueStore.put('rawbytes-to-retransmit', {});
}
});
}
Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async {
Future<void> addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async {
await lockRetransStore.protect(() async {
var retransmit = await getRetransmission();
final retransmit = await getRetransmission();
retransmit[seq.toString()] = base64Encode(bytes);
KeyValueStore.put("rawbytes-to-retransmit", retransmit);
await KeyValueStore.put('rawbytes-to-retransmit', retransmit);
});
}
Future removeFromRetransmissionBuffer(Int64 seq) async {
Future<void> removeFromRetransmissionBuffer(Int64 seq) async {
await lockRetransStore.protect(() async {
var retransmit = await getRetransmission();
final retransmit = await getRetransmission();
if (retransmit.isEmpty) return;
retransmit.remove(seq.toString());
KeyValueStore.put("rawbytes-to-retransmit", retransmit);
await KeyValueStore.put('rawbytes-to-retransmit', retransmit);
});
}
@ -287,11 +290,11 @@ class ApiService {
final requestBytes = request.writeToBuffer();
if (ensureRetransmission) {
addToRetransmissionBuffer(seq, requestBytes);
await addToRetransmissionBuffer(seq, requestBytes);
}
if (_channel == null) {
Log.warn("sending request while api is not connected");
Log.warn('sending request while api is not connected');
if (!await connect()) {
return Result.error(ErrorCode.InternalError);
}
@ -302,12 +305,12 @@ class ApiService {
_channel!.sink.add(requestBytes);
Result res = asResult(await _waitForResponse(seq));
final res = asResult(await _waitForResponse(seq));
if (res.isError) {
Log.error("got error from server: ${res.error}");
Log.error('got error from server: ${res.error}');
if (res.error == ErrorCode.AppVersionOutdated) {
globalCallbackAppIsOutdated();
Log.error("App Version is OUTDATED.");
Log.error('App Version is OUTDATED.');
appIsOutdated = true;
await close(() {});
return Result.error(ErrorCode.InternalError);
@ -320,16 +323,16 @@ class ApiService {
// this will send the request one more time.
return sendRequestSync(request, authenticated: false);
} else {
Log.error("session is not authenticated");
Log.error('session is not authenticated');
return Result.error(ErrorCode.InternalError);
}
}
}
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
Log.error("Contact deleted their account $contactId.");
Log.error('Contact deleted their account $contactId.');
await twonlyDB.contactsDao.updateContact(
contactId,
ContactsCompanion(
const ContactsCompanion(
deleted: Value(true),
),
);
@ -339,8 +342,8 @@ class ApiService {
}
Future<bool> tryAuthenticateWithToken(int userId) async {
final storage = FlutterSecureStorage();
String? apiAuthToken =
const storage = FlutterSecureStorage();
final apiAuthToken =
await storage.read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthToken != null) {
@ -355,21 +358,21 @@ class ApiService {
final result = await sendRequestSync(req, authenticated: false);
if (result.isSuccess) {
server.Response_Ok ok = result.value;
final ok = result.value as server.Response_Ok;
if (ok.hasAuthenticated()) {
server.Response_Authenticated authenticated = ok.authenticated;
updateUserdata((user) {
final authenticated = ok.authenticated;
await updateUserdata((user) {
user.subscriptionPlan = authenticated.plan;
return user;
});
}
Log.info("websocket is authenticated");
onAuthenticated();
Log.info('websocket is authenticated');
unawaited(onAuthenticated());
return true;
}
if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid) {
Log.error("got error while authenticating to the server", result);
Log.error('got error while authenticating to the server', result);
return false;
}
}
@ -377,7 +380,7 @@ class ApiService {
return false;
}
Future authenticate() async {
Future<void> authenticate() async {
if (isAuthenticated) return;
if (await getSignalIdentity() == null) {
return;
@ -392,11 +395,11 @@ class ApiService {
var handshake = Handshake()
..getauthchallenge = Handshake_GetAuthChallenge();
var req = createClientToServerFromHandshake(handshake);
final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false);
if (result.isError) {
Log.error("could not request auth challenge", result);
Log.error('could not request auth challenge', result);
return;
}
@ -405,7 +408,7 @@ class ApiService {
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) return;
final random = getRandomUint8List(32);
final signature = sign(privKey.serialize(), challenge, random);
final signature = sign(privKey.serialize(), challenge as Uint8List, random);
privKey = null;
final getAuthToken = Handshake_GetAuthToken()
@ -414,18 +417,18 @@ class ApiService {
final getauthtoken = Handshake()..getauthtoken = getAuthToken;
var req2 = createClientToServerFromHandshake(getauthtoken);
final req2 = createClientToServerFromHandshake(getauthtoken);
final result2 = await sendRequestSync(req2, authenticated: false);
if (result2.isError) {
Log.error("could not send auth response: ${result2.error}");
Log.error('could not send auth response: ${result2.error}');
return;
}
Uint8List apiAuthToken = result2.value.authtoken;
String apiAuthTokenB64 = base64Encode(apiAuthToken);
final apiAuthToken = result2.value.authtoken as Uint8List;
final apiAuthTokenB64 = base64Encode(apiAuthToken);
final storage = FlutterSecureStorage();
const storage = FlutterSecureStorage();
await storage.write(
key: SecureStorageKeys.apiAuthToken, value: apiAuthTokenB64);
@ -452,51 +455,51 @@ class ApiService {
..signedPrekeyId = Int64(signedPreKey.id)
..isIos = Platform.isIOS;
if (inviteCode != null && inviteCode != "") {
if (inviteCode != null && inviteCode != '') {
register.inviteCode = inviteCode;
}
var handshake = Handshake()..register = register;
var req = createClientToServerFromHandshake(handshake);
final handshake = Handshake()..register = register;
final req = createClientToServerFromHandshake(handshake);
return await sendRequestSync(req);
return sendRequestSync(req);
}
Future<Result> getUsername(int userId) async {
var get = ApplicationData_GetUserById()..userId = Int64(userId);
var appData = ApplicationData()..getuserbyid = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, contactId: userId);
final get = ApplicationData_GetUserById()..userId = Int64(userId);
final appData = ApplicationData()..getuserbyid = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req, contactId: userId);
}
Future<Result> downloadDone(List<int> token) async {
var get = ApplicationData_DownloadDone()..downloadToken = token;
var appData = ApplicationData()..downloaddone = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, ensureRetransmission: true);
final get = ApplicationData_DownloadDone()..downloadToken = token;
final appData = ApplicationData()..downloaddone = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req, ensureRetransmission: true);
}
Future<Result> getCurrentLocation() async {
var get = ApplicationData_GetLocation();
var appData = ApplicationData()..getlocation = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_GetLocation();
final appData = ApplicationData()..getlocation = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> getUserData(String username) async {
var get = ApplicationData_GetUserByUsername()..username = username;
var appData = ApplicationData()..getuserbyusername = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_GetUserByUsername()..username = username;
final appData = ApplicationData()..getuserbyusername = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Response_PlanBallance?> getPlanBallance() async {
var get = ApplicationData_GetCurrentPlanInfos();
var appData = ApplicationData()..getcurrentplaninfos = get;
var req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req);
final get = ApplicationData_GetCurrentPlanInfos();
final appData = ApplicationData()..getcurrentplaninfos = get;
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
server.Response_Ok ok = res.value;
final ok = res.value as server.Response_Ok;
if (ok.hasPlanballance()) {
return ok.planballance;
}
@ -505,12 +508,12 @@ class ApiService {
}
Future<Response_Vouchers?> getVoucherList() async {
var get = ApplicationData_GetVouchers();
var appData = ApplicationData()..getvouchers = get;
var req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req);
final get = ApplicationData_GetVouchers();
final appData = ApplicationData()..getvouchers = get;
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
server.Response_Ok ok = res.value;
final ok = res.value as server.Response_Ok;
if (ok.hasVouchers()) {
return ok.vouchers;
}
@ -519,12 +522,12 @@ class ApiService {
}
Future<List<Response_AddAccountsInvite>?> getAdditionalUserInvites() async {
var get = ApplicationData_GetAddAccountsInvites();
var appData = ApplicationData()..getaddaccountsinvites = get;
var req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req);
final get = ApplicationData_GetAddAccountsInvites();
final appData = ApplicationData()..getaddaccountsinvites = get;
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
server.Response_Ok ok = res.value;
final ok = res.value as server.Response_Ok;
if (ok.hasAddaccountsinvites()) {
return ok.addaccountsinvites.invites;
}
@ -533,63 +536,63 @@ class ApiService {
}
Future<Result> updatePlanOptions(bool autoRenewal) async {
var get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal;
var appData = ApplicationData()..updateplanoptions = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_UpdatePlanOptions()..autoRenewal = autoRenewal;
final appData = ApplicationData()..updateplanoptions = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> removeAdditionalUser(Int64 userId) async {
var get = ApplicationData_RemoveAdditionalUser()..userId = userId;
var appData = ApplicationData()..removeadditionaluser = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, contactId: userId.toInt());
final get = ApplicationData_RemoveAdditionalUser()..userId = userId;
final appData = ApplicationData()..removeadditionaluser = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req, contactId: userId.toInt());
}
Future<Result> buyVoucher(int valueInCents) async {
var get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
var appData = ApplicationData()..createvoucher = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_CreateVoucher()..valueCents = valueInCents;
final appData = ApplicationData()..createvoucher = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> switchToPayedPlan(
String planId, bool payMonthly, bool autoRenewal) async {
var get = ApplicationData_SwitchToPayedPlan()
final get = ApplicationData_SwitchToPayedPlan()
..planId = planId
..payMonthly = payMonthly
..autoRenewal = autoRenewal;
var appData = ApplicationData()..switchtopayedplan = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final appData = ApplicationData()..switchtopayedplan = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> redeemVoucher(String voucher) async {
var get = ApplicationData_RedeemVoucher()..voucher = voucher;
var appData = ApplicationData()..redeemvoucher = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_RedeemVoucher()..voucher = voucher;
final appData = ApplicationData()..redeemvoucher = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> deleteAccount() async {
var get = ApplicationData_DeleteAccount();
var appData = ApplicationData()..deleteaccount = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_DeleteAccount();
final appData = ApplicationData()..deleteaccount = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> redeemUserInviteCode(String inviteCode) async {
var get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode;
var appData = ApplicationData()..redeemadditionalcode = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_RedeemAdditionalCode()..inviteCode = inviteCode;
final appData = ApplicationData()..redeemadditionalcode = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> updateFCMToken(String googleFcm) async {
var get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
var appData = ApplicationData()..updategooglefcmtoken = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm;
final appData = ApplicationData()..updategooglefcmtoken = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> updateSignedPreKey(
@ -597,22 +600,23 @@ class ApiService {
Uint8List signedPreKey,
Uint8List signedPreKeySignature,
) async {
var get = ApplicationData_UpdateSignedPreKey()
final get = ApplicationData_UpdateSignedPreKey()
..signedPrekeyId = Int64(signedPreKeyId)
..signedPrekey = signedPreKey
..signedPrekeySignature = signedPreKeySignature;
var appData = ApplicationData()..updatesignedprekey = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
final appData = ApplicationData()..updatesignedprekey = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Response_SignedPreKey?> getSignedKeyByUserId(int userId) async {
var get = ApplicationData_GetSignedPreKeyByUserId()..userId = Int64(userId);
var appData = ApplicationData()..getsignedprekeybyuserid = get;
var req = createClientToServerFromApplicationData(appData);
final get = ApplicationData_GetSignedPreKeyByUserId()
..userId = Int64(userId);
final appData = ApplicationData()..getsignedprekeybyuserid = get;
final req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req, contactId: userId);
if (res.isSuccess) {
server.Response_Ok ok = res.value;
final ok = res.value as server.Response_Ok;
if (ok.hasSignedprekey()) {
return ok.signedprekey;
}
@ -621,12 +625,12 @@ class ApiService {
}
Future<OtherPreKeys?> getPreKeysByUserId(int userId) async {
var get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId);
var appData = ApplicationData()..getprekeysbyuserid = get;
var req = createClientToServerFromApplicationData(appData);
final get = ApplicationData_GetPrekeysByUserId()..userId = Int64(userId);
final appData = ApplicationData()..getprekeysbyuserid = get;
final req = createClientToServerFromApplicationData(appData);
Result res = await sendRequestSync(req, contactId: userId);
if (res.isSuccess) {
server.Response_Ok ok = res.value;
final ok = res.value as server.Response_Ok;
if (ok.hasUserdata()) {
server.Response_UserData data = ok.userdata;
if (data.hasSignedPrekey() &&
@ -654,8 +658,8 @@ class ApiService {
testMessage.pushData = pushData;
}
var appData = ApplicationData()..textmessage = testMessage;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, contactId: target);
final appData = ApplicationData()..textmessage = testMessage;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req, contactId: target);
}
}

View file

@ -1,18 +1,20 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
@ -20,13 +22,13 @@ import 'package:twonly/src/views/camera/share_image_editor_view.dart';
Map<int, DateTime> downloadStartedForMediaReceived = {};
Future tryDownloadAllMediaFiles({bool force = false}) async {
Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
// This is called when WebSocket is newly connected, so allow all downloads to be restarted.
downloadStartedForMediaReceived = {};
List<Message> messages =
final messages =
await twonlyDB.messagesDao.getAllMessagesPendingDownloading();
for (Message message in messages) {
for (final message in messages) {
await startDownloadMedia(message, force);
}
}
@ -45,8 +47,7 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
};
Future<bool> isAllowedToDownload(bool isVideo) async {
final List<ConnectivityResult> connectivityResult =
await (Connectivity().checkConnectivity());
final connectivityResult = await Connectivity().checkConnectivity();
final user = await getUser();
final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions;
@ -76,9 +77,9 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
return false;
}
Future handleDownloadStatusUpdate(TaskStatusUpdate update) async {
int messageId = int.parse(update.task.taskId.replaceAll("download_", ""));
bool failed = false;
Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
final messageId = int.parse(update.task.taskId.replaceAll('download_', ''));
var failed = false;
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled ||
@ -90,46 +91,47 @@ Future handleDownloadStatusUpdate(TaskStatusUpdate update) async {
} else {
failed = true;
Log.error(
"Got invalid response status code: ${update.responseStatusCode}",
'Got invalid response status code: ${update.responseStatusCode}',
);
}
} else {
Log.info("Got ${update.status} for $messageId");
Log.info('Got ${update.status} for $messageId');
return;
}
await handleDownloadStatusUpdateInternal(messageId, failed);
}
Future handleDownloadStatusUpdateInternal(int messageId, bool failed) async {
Future<void> handleDownloadStatusUpdateInternal(
int messageId, bool failed) async {
if (failed) {
Log.error("Download failed for $messageId");
Message? message = await twonlyDB.messagesDao
Log.error('Download failed for $messageId');
final message = await twonlyDB.messagesDao
.getMessageByMessageId(messageId)
.getSingleOrNull();
if (message != null) {
await handleMediaError(message);
}
} else {
Log.info("Download was successfully for $messageId");
Log.info('Download was successfully for $messageId');
await handleEncryptedFile(messageId);
}
}
Future startDownloadMedia(Message message, bool force,
Future<void> startDownloadMedia(Message message, bool force,
{int retryCounter = 0}) async {
if (message.contentJson == null) return;
if (downloadStartedForMediaReceived[message.messageId] != null &&
retryCounter == 0) {
DateTime started = downloadStartedForMediaReceived[message.messageId]!;
Duration elapsed = DateTime.now().difference(started);
if (elapsed <= Duration(seconds: 60)) {
Log.error("Download already started...");
final started = downloadStartedForMediaReceived[message.messageId]!;
final elapsed = DateTime.now().difference(started);
if (elapsed <= const Duration(seconds: 60)) {
Log.error('Download already started...');
return;
}
}
final content =
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
final content = MessageContent.fromJson(
message.kind, jsonDecode(message.contentJson!) as Map);
if (content is! MediaMessageContent) return;
if (content.downloadToken == null) return;
@ -158,7 +160,7 @@ Future startDownloadMedia(Message message, bool force,
if (message.downloadState != DownloadState.downloaded) {
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(
const MessagesCompanion(
downloadState: Value(DownloadState.downloading),
),
);
@ -166,36 +168,34 @@ Future startDownloadMedia(Message message, bool force,
downloadStartedForMediaReceived[message.messageId] = DateTime.now();
String downloadToken = uint8ListToHex(content.downloadToken!);
final downloadToken = uint8ListToHex(content.downloadToken!);
String apiUrl =
"http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken";
final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken';
try {
final task = DownloadTask(
url: apiUrl,
taskId: "download_${media.messageId}",
directory: "media/received/",
taskId: 'download_${media.messageId}',
directory: 'media/received/',
baseDirectory: BaseDirectory.applicationSupport,
filename: "${media.messageId}.encrypted",
filename: '${media.messageId}.encrypted',
priority: 0,
retries: 10,
);
Log.info(
"Got media file. Starting download: ${downloadToken.substring(0, 10)}",
'Got media file. Starting download: ${downloadToken.substring(0, 10)}',
);
try {
await downloadFileFast(media.messageId, apiUrl);
return;
} catch (e) {
Log.error("Fast download failed: $e");
final result = await FileDownloader().enqueue(task);
return result;
Log.error('Fast download failed: $e');
await FileDownloader().enqueue(task);
}
} catch (e) {
Log.error("Exception during download: $e");
Log.error('Exception during download: $e');
}
}
@ -203,19 +203,19 @@ Future<void> downloadFileFast(
int messageId,
String apiUrl,
) async {
final String directoryPath =
"${(await getApplicationSupportDirectory()).path}/media/received/";
final String filename = "$messageId.encrypted";
final directoryPath =
'${(await getApplicationSupportDirectory()).path}/media/received/';
final filename = '$messageId.encrypted';
final Directory directory = Directory(directoryPath);
if (!await directory.exists()) {
final directory = Directory(directoryPath);
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
final String filePath = "${directory.path}/$filename";
final filePath = '${directory.path}/$filename';
final response =
await http.get(Uri.parse(apiUrl)).timeout(Duration(seconds: 10));
await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
await File(filePath).writeAsBytes(response.bodyBytes);
@ -228,34 +228,34 @@ Future<void> downloadFileFast(
return;
}
// can be tried again
throw Exception("Fast download failed with status: ${response.statusCode}");
throw Exception('Fast download failed with status: ${response.statusCode}');
}
}
Future handleEncryptedFile(int messageId) async {
Message? msg = await twonlyDB.messagesDao
Future<void> handleEncryptedFile(int messageId) async {
final msg = await twonlyDB.messagesDao
.getMessageByMessageId(messageId)
.getSingleOrNull();
if (msg == null) {
Log.error("Not message for downloaded file found: $messageId");
Log.error('Not message for downloaded file found: $messageId');
return;
}
Uint8List? encryptedBytes = await readMediaFile(msg.messageId, "encrypted");
final encryptedBytes = await readMediaFile(msg.messageId, 'encrypted');
if (encryptedBytes == null) {
Log.error("encrypted bytes are not found for ${msg.messageId}");
Log.error('encrypted bytes are not found for ${msg.messageId}');
return;
}
MediaMessageContent content =
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!));
final content =
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map);
try {
final chacha20 = FlutterChacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!);
final secretKeyData = SecretKeyData(content.encryptionKey!);
SecretBox secretBox = SecretBox(
final secretBox = SecretBox(
encryptedBytes,
nonce: content.encryptionNonce!,
mac: Mac(content.encryptionMac!),
@ -269,10 +269,10 @@ Future handleEncryptedFile(int messageId) async {
if (content.isVideo) {
final extractedBytes = extractUint8Lists(imageBytes);
imageBytes = extractedBytes[0];
await writeMediaFile(msg.messageId, "mp4", extractedBytes[1]);
await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]);
}
await writeMediaFile(msg.messageId, "png", imageBytes);
await writeMediaFile(msg.messageId, 'png', imageBytes);
// } catch (e) {
// Log.error(
// "could not decrypt the media file in the second try. reporting error to user: $e");
@ -280,13 +280,13 @@ Future handleEncryptedFile(int messageId) async {
// return;
// }
} catch (e) {
Log.error("$e");
Log.error('$e');
/// legacy support
final chacha20 = Xchacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!);
final secretKeyData = SecretKeyData(content.encryptionKey!);
SecretBox secretBox = SecretBox(
final secretBox = SecretBox(
encryptedBytes,
nonce: content.encryptionNonce!,
mac: Mac(content.encryptionMac!),
@ -300,121 +300,122 @@ Future handleEncryptedFile(int messageId) async {
if (content.isVideo) {
final extractedBytes = extractUint8Lists(imageBytes);
imageBytes = extractedBytes[0];
await writeMediaFile(msg.messageId, "mp4", extractedBytes[1]);
await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]);
}
await writeMediaFile(msg.messageId, "png", imageBytes);
await writeMediaFile(msg.messageId, 'png', imageBytes);
} catch (e) {
Log.error(
"could not decrypt the media file in the second try. reporting error to user: $e");
handleMediaError(msg);
'could not decrypt the media file in the second try. reporting error to user: $e');
await handleMediaError(msg);
return;
}
}
await twonlyDB.messagesDao.updateMessageByMessageId(
msg.messageId,
MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
const MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
);
Log.info("Download and decryption of ${msg.messageId} was successful");
Log.info('Download and decryption of ${msg.messageId} was successful');
await deleteMediaFile(msg.messageId, "encrypted");
await deleteMediaFile(msg.messageId, 'encrypted');
apiService.downloadDone(content.downloadToken!);
unawaited(apiService.downloadDone(content.downloadToken!));
}
Future<Uint8List?> getImageBytes(int mediaId) async {
return await readMediaFile(mediaId, "png");
return readMediaFile(mediaId, 'png');
}
Future<File?> getVideoPath(int mediaId) async {
String basePath = await getMediaFilePath(mediaId, "received");
return File("$basePath.mp4");
final basePath = await getMediaFilePath(mediaId, 'received');
return File('$basePath.mp4');
}
/// --- helper functions ---
Future<Uint8List?> readMediaFile(int mediaId, String type) async {
String basePath = await getMediaFilePath(mediaId, "received");
File file = File("$basePath.$type");
Log.info("Reading: $file");
if (!await file.exists()) {
final basePath = await getMediaFilePath(mediaId, 'received');
final file = File('$basePath.$type');
Log.info('Reading: $file');
if (!file.existsSync()) {
return null;
}
return await file.readAsBytes();
return file.readAsBytes();
}
Future<bool> existsMediaFile(int mediaId, String type) async {
String basePath = await getMediaFilePath(mediaId, "received");
File file = File("$basePath.$type");
return await file.exists();
final basePath = await getMediaFilePath(mediaId, 'received');
final file = File('$basePath.$type');
return file.existsSync();
}
Future<void> writeMediaFile(int mediaId, String type, Uint8List data) async {
String basePath = await getMediaFilePath(mediaId, "received");
File file = File("$basePath.$type");
final basePath = await getMediaFilePath(mediaId, 'received');
final file = File('$basePath.$type');
await file.writeAsBytes(data);
}
Future<void> deleteMediaFile(int mediaId, String type) async {
String basePath = await getMediaFilePath(mediaId, "received");
File file = File("$basePath.$type");
final basePath = await getMediaFilePath(mediaId, 'received');
final file = File('$basePath.$type');
try {
if (await file.exists()) {
if (file.existsSync()) {
await file.delete();
}
} catch (e) {
Log.error("Error deleting: $e");
Log.error('Error deleting: $e');
}
}
Future<void> purgeReceivedMediaFiles() async {
final basedir = await getApplicationSupportDirectory();
final directory = Directory(join(basedir.path, 'media', "received"));
final directory = Directory(join(basedir.path, 'media', 'received'));
await purgeMediaFiles(directory);
}
Future<void> purgeMediaFiles(Directory directory) async {
// Check if the directory exists
if (await directory.exists()) {
if (directory.existsSync()) {
// List all files in the directory
List<FileSystemEntity> files = directory.listSync();
final files = directory.listSync();
// Iterate over each file
for (var file in files) {
for (final file in files) {
// Get the filename
String filename = file.uri.pathSegments.last;
final filename = file.uri.pathSegments.last;
// Use a regular expression to extract the integer part
final match = RegExp(r'(\d+)').firstMatch(filename);
if (match != null) {
// Parse the integer and add it to the list
int fileId = int.parse(match.group(0)!);
final fileId = int.parse(match.group(0)!);
try {
if (directory.path.endsWith("send")) {
List<Message> messages =
if (directory.path.endsWith('send')) {
final messages =
await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId);
bool canBeDeleted = true;
var canBeDeleted = true;
for (final message in messages) {
try {
MediaMessageContent content = MediaMessageContent.fromJson(
jsonDecode(message.contentJson!),
final content = MediaMessageContent.fromJson(
jsonDecode(message.contentJson!) as Map,
);
DateTime oneDayAgo = DateTime.now().subtract(Duration(days: 1));
DateTime twoDaysAgo =
DateTime.now().subtract(Duration(days: 1));
final oneDayAgo =
DateTime.now().subtract(const Duration(days: 1));
final twoDaysAgo =
DateTime.now().subtract(const Duration(days: 1));
if (((message.openedAt == null ||
if ((message.openedAt == null ||
oneDayAgo.isBefore(message.openedAt!)) &&
!message.errorWhileSending)) {
!message.errorWhileSending) {
canBeDeleted = false;
} else if (message.mediaStored) {
if (!file.path.contains(".original.") &&
!file.path.contains(".encrypted")) {
if (!file.path.contains('.original.') &&
!file.path.contains('.encrypted')) {
canBeDeleted = false;
}
}
@ -429,8 +430,8 @@ Future<void> purgeMediaFiles(Directory directory) async {
canBeDeleted = true;
}
// Encrypted or upload data can be removed when acknowledgeByServer
if (file.path.contains(".upload") ||
file.path.contains(".encrypted")) {
if (file.path.contains('.upload') ||
file.path.contains('.encrypted')) {
canBeDeleted = true;
}
}
@ -439,11 +440,11 @@ Future<void> purgeMediaFiles(Directory directory) async {
}
}
if (canBeDeleted) {
Log.info("purged media file ${file.path} ");
Log.info('purged media file ${file.path} ');
file.deleteSync();
}
} else {
Message? message = await twonlyDB.messagesDao
final message = await twonlyDB.messagesDao
.getMessageByMessageId(fileId)
.getSingleOrNull();
if ((message == null) ||
@ -455,7 +456,7 @@ Future<void> purgeMediaFiles(Directory directory) async {
}
}
} catch (e) {
Log.error("$e");
Log.error('$e');
}
}
}

View file

@ -1,15 +1,16 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:background_downloader/background_downloader.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart';
import 'package:path/path.dart';
@ -35,11 +36,11 @@ import 'package:video_compress/video_compress.dart';
Future<ErrorCode?> isAllowedToSend() async {
final user = await getUser();
if (user == null) return null;
if (user.subscriptionPlan == "Preview") {
if (user.subscriptionPlan == 'Preview') {
return ErrorCode.PlanNotAllowed;
}
if (user.subscriptionPlan == "Free") {
int? todaysImageCounter = user.todaysImageCounter;
if (user.subscriptionPlan == 'Free') {
var todaysImageCounter = user.todaysImageCounter;
if (user.lastImageSend != null && user.todaysImageCounter != null) {
if (isToday(user.lastImageSend!)) {
if (user.todaysImageCounter == 3) {
@ -53,25 +54,26 @@ Future<ErrorCode?> isAllowedToSend() async {
todaysImageCounter = 1;
}
await updateUserdata((user) {
user.lastImageSend = DateTime.now();
user.todaysImageCounter = todaysImageCounter;
user
..lastImageSend = DateTime.now()
..todaysImageCounter = todaysImageCounter;
return user;
});
}
return null;
}
Future initFileDownloader() async {
Future<void> initFileDownloader() async {
FileDownloader().updates.listen((update) async {
switch (update) {
case TaskStatusUpdate():
if (update.task.taskId.contains("upload_")) {
if (update.task.taskId.contains('upload_')) {
await handleUploadStatusUpdate(update);
}
if (update.task.taskId.contains("download_")) {
if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update);
}
if (update.task.taskId.contains("backup")) {
if (update.task.taskId.contains('backup')) {
await handleBackupStatusUpdate(update);
}
case TaskProgressUpdate():
@ -82,17 +84,16 @@ Future initFileDownloader() async {
await FileDownloader().start();
FileDownloader().configure(androidConfig: [
await FileDownloader().configure(androidConfig: [
(Config.bypassTLSCertificateValidation, kDebugMode),
]);
if (kDebugMode) {
FileDownloader().configureNotification(
running: TaskNotification(
running: const TaskNotification(
'Uploading/Downloading',
'{filename} ({progress}).',
),
complete: null,
progressBar: true,
);
}
@ -112,14 +113,14 @@ Future initFileDownloader() async {
Future<bool> checkForFailedUploads() async {
final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
List<int> mediaUploadIds = [];
for (Message message in messages) {
final mediaUploadIds = <int>[];
for (var message in messages) {
if (mediaUploadIds.contains(message.mediaUploadId)) {
continue;
}
int affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
message.mediaUploadId!,
MediaUploadsCompanion(
const MediaUploadsCompanion(
state: Value(UploadState.pending),
encryptionData: Value(
null, // start from scratch e.q. encrypt the files again if already happen
@ -128,11 +129,11 @@ Future<bool> checkForFailedUploads() async {
);
if (affectedRows == 0) {
Log.error(
"The media from message ${message.messageId} already deleted.",
'The media from message ${message.messageId} already deleted.',
);
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(
const MessagesCompanion(
errorWhileSending: Value(true),
),
);
@ -142,24 +143,24 @@ Future<bool> checkForFailedUploads() async {
}
if (messages.isNotEmpty) {
Log.error(
"Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.",
'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.',
);
}
return mediaUploadIds.isNotEmpty; // return true if there are affected
}
final lockingHandleMediaFile = Mutex();
Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
if (maxRetries == 0) {
Log.error("retried media upload 3 times. abort retrying");
Log.error('retried media upload 3 times. abort retrying');
return;
}
bool retry = await lockingHandleMediaFile.protect<bool>(() async {
final retry = await lockingHandleMediaFile.protect<bool>(() async {
final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry();
if (mediaFiles.isEmpty) {
return checkForFailedUploads();
}
Log.info("re uploading ${mediaFiles.length} media files.");
Log.info('re uploading ${mediaFiles.length} media files.');
for (final mediaFile in mediaFiles) {
if (mediaFile.messageIds == null || mediaFile.metadata == null) {
if (appRestarted) {
@ -168,7 +169,7 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
await twonlyDB.mediaUploadsDao
.deleteMediaUpload(mediaFile.mediaUploadId);
Log.info(
"upload can be removed, the finalized function was never called...",
'upload can be removed, the finalized function was never called...',
);
}
continue;
@ -188,24 +189,23 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
}
Future<int?> initMediaUpload() async {
return await twonlyDB.mediaUploadsDao
.insertMediaUpload(MediaUploadsCompanion());
return twonlyDB.mediaUploadsDao
.insertMediaUpload(const MediaUploadsCompanion());
}
Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
await videoFilePath.copy("$basePath.original.mp4");
return await compressVideoIfExists(mediaUploadId);
final basePath = await getMediaFilePath(mediaUploadId, 'send');
await videoFilePath.copy('$basePath.original.mp4');
return compressVideoIfExists(mediaUploadId);
}
Future<Uint8List> addOrModifyImageToUpload(
int mediaUploadId, Uint8List imageBytes) async {
Uint8List imageBytesCompressed;
Stopwatch stopwatch = Stopwatch();
stopwatch.start();
final stopwatch = Stopwatch()..start();
Log.info("Raw images size in bytes: ${imageBytes.length}");
Log.info('Raw images size in bytes: ${imageBytes.length}');
try {
imageBytesCompressed = await FlutterImageCompress.compressWithList(
@ -224,11 +224,11 @@ Future<Uint8List> addOrModifyImageToUpload(
quality: 60,
);
}
await writeSendMediaFile(mediaUploadId, "png", imageBytesCompressed);
await writeSendMediaFile(mediaUploadId, 'png', imageBytesCompressed);
} catch (e) {
Log.error("$e");
Log.error('$e');
// as a fall back use the original image
await writeSendMediaFile(mediaUploadId, "png", imageBytes);
await writeSendMediaFile(mediaUploadId, 'png', imageBytes);
imageBytesCompressed = imageBytes;
}
@ -236,7 +236,7 @@ Future<Uint8List> addOrModifyImageToUpload(
Log.info(
'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds');
Log.info("Raw images size in bytes: ${imageBytesCompressed.length}");
Log.info('Raw images size in bytes: ${imageBytesCompressed.length}');
// stopwatch.reset();
// stopwatch.start();
@ -256,16 +256,16 @@ Future<Uint8List> addOrModifyImageToUpload(
/// remove the data so it will be done again.
await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId,
MediaUploadsCompanion(
const MediaUploadsCompanion(
encryptionData: Value(null),
),
);
return imageBytesCompressed;
}
Future handlePreProcessingState(MediaUpload media) async {
Future<void> handlePreProcessingState(MediaUpload media) async {
try {
final imageHandler = readSendMediaFile(media.mediaUploadId, "png");
final imageHandler = readSendMediaFile(media.mediaUploadId, 'png');
final videoHandler = compressVideoIfExists(media.mediaUploadId);
await encryptMediaFiles(
media.mediaUploadId,
@ -273,34 +273,36 @@ Future handlePreProcessingState(MediaUpload media) async {
videoHandler,
);
} catch (e) {
Log.error("${media.mediaUploadId} got error in pre processing: $e");
Log.error('${media.mediaUploadId} got error in pre processing: $e');
await handleUploadError(media);
}
}
Future encryptMediaFiles(
Future<void> encryptMediaFiles(
int mediaUploadId,
Future imageHandler,
Future<Uint8List> imageHandler,
Future<bool>? videoHandler,
) async {
Log.info("$mediaUploadId encrypting files");
Uint8List dataToEncrypt = await imageHandler;
Log.info('$mediaUploadId encrypting files');
// ignore: cast_nullable_to_non_nullable
var dataToEncrypt = await imageHandler;
/// if there is a video wait until it is finished with compression
if (videoHandler != null) {
if (await videoHandler) {
Uint8List compressedVideo = await readSendMediaFile(mediaUploadId, "mp4");
final compressedVideo = await readSendMediaFile(mediaUploadId, 'mp4');
dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo);
}
}
var state = MediaEncryptionData();
final state = MediaEncryptionData();
final chacha20 = FlutterChacha20.poly1305Aead();
SecretKeyData secretKey = await (await chacha20.newSecretKey()).extract();
final secretKey = await (await chacha20.newSecretKey()).extract();
state.encryptionKey = secretKey.bytes;
state.encryptionNonce = chacha20.newNonce();
state
..encryptionKey = secretKey.bytes
..encryptionNonce = chacha20.newNonce();
final secretBox = await chacha20.encrypt(
dataToEncrypt,
@ -308,46 +310,46 @@ Future encryptMediaFiles(
nonce: state.encryptionNonce,
);
state.encryptionMac = secretBox.mac.bytes;
state.sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
state
..encryptionMac = secretBox.mac.bytes
..sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
final encryptedBytes = Uint8List.fromList(secretBox.cipherText);
await writeSendMediaFile(
mediaUploadId,
"encrypted",
'encrypted',
encryptedBytes,
);
await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId,
MediaUploadsCompanion(
state: Value(UploadState.readyToUpload),
state: const Value(UploadState.readyToUpload),
encryptionData: Value(state),
),
);
handleNextMediaUploadSteps(mediaUploadId);
unawaited(handleNextMediaUploadSteps(mediaUploadId));
}
Future finalizeUpload(int mediaUploadId, List<int> contactIds,
Future<void> finalizeUpload(int mediaUploadId, List<int> contactIds,
bool isRealTwonly, bool isVideo, bool mirrorVideo, int maxShowTime) async {
MediaUploadMetadata metadata = MediaUploadMetadata();
metadata.contactIds = contactIds;
metadata.isRealTwonly = isRealTwonly;
metadata.messageSendAt = DateTime.now();
metadata.isVideo = isVideo;
metadata.maxShowTime = maxShowTime;
metadata.mirrorVideo = mirrorVideo;
final metadata = MediaUploadMetadata()
..contactIds = contactIds
..isRealTwonly = isRealTwonly
..messageSendAt = DateTime.now()
..isVideo = isVideo
..maxShowTime = maxShowTime
..mirrorVideo = mirrorVideo;
List<int> messageIds = [];
final messageIds = <int>[];
for (final contactId in contactIds) {
int? messageId = await twonlyDB.messagesDao.insertMessage(
final messageId = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(contactId),
kind: Value(MessageKind.media),
kind: const Value(MessageKind.media),
sendAt: Value(metadata.messageSendAt),
downloadState: Value(DownloadState.pending),
downloadState: const Value(DownloadState.pending),
mediaUploadId: Value(mediaUploadId),
contentJson: Value(
jsonEncode(
@ -364,14 +366,14 @@ Future finalizeUpload(int mediaUploadId, List<int> contactIds,
// de-archive contact when sending a new message
await twonlyDB.contactsDao.updateContact(
contactId,
ContactsCompanion(
const ContactsCompanion(
archived: Value(false),
),
);
if (messageId != null) {
messageIds.add(messageId);
} else {
Log.error("Error inserting media upload message in database.");
Log.error('Error inserting media upload message in database.');
}
}
@ -383,13 +385,13 @@ Future finalizeUpload(int mediaUploadId, List<int> contactIds,
),
);
handleNextMediaUploadSteps(mediaUploadId);
unawaited(handleNextMediaUploadSteps(mediaUploadId));
}
final lockingHandleNextMediaUploadStep = Mutex();
Future handleNextMediaUploadSteps(int mediaUploadId) async {
Future<void> handleNextMediaUploadSteps(int mediaUploadId) async {
await lockingHandleNextMediaUploadStep.protect(() async {
var mediaUpload = await twonlyDB.mediaUploadsDao
final mediaUpload = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId)
.getSingleOrNull();
@ -397,7 +399,7 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
if (mediaUpload.state == UploadState.receiverNotified ||
mediaUpload.state == UploadState.uploadTaskStarted) {
/// Upload done and all users are notified :)
Log.info("$mediaUploadId is already done");
Log.info('$mediaUploadId is already done');
return false;
}
try {
@ -414,7 +416,7 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
await handleMediaUpload(mediaUpload);
} catch (e) {
Log.error("Non recoverable error while sending media file: $e");
Log.error('Non recoverable error while sending media file: $e');
await handleUploadError(mediaUpload);
}
return false;
@ -427,22 +429,22 @@ Future handleNextMediaUploadSteps(int mediaUploadId) async {
///
///
Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
bool failed = false;
int mediaUploadId = int.parse(update.task.taskId.replaceAll("upload_", ""));
Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
var failed = false;
final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', ''));
MediaUpload? media = await twonlyDB.mediaUploadsDao
final media = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId)
.getSingleOrNull();
if (media == null) {
Log.error(
"Got an upload task but no upload media in the media upload database",
'Got an upload task but no upload media in the media upload database',
);
return;
}
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
Log.error("Upload failed: ${update.status}");
Log.error('Upload failed: ${update.status}');
failed = true;
} else if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) {
@ -454,7 +456,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
failed = true;
}
Log.error(
"Got error while uploading: ${update.responseStatusCode}",
'Got error while uploading: ${update.responseStatusCode}',
);
}
}
@ -463,7 +465,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
for (final messageId in media.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId(
messageId,
MessagesCompanion(
const MessagesCompanion(
acknowledgeByServer: Value(true),
errorWhileSending: Value(true),
),
@ -474,13 +476,13 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async {
'Status update for ${update.task.taskId} with status ${update.status}');
}
Future handleUploadSuccess(MediaUpload media) async {
Log.info("Upload of ${media.mediaUploadId} success!");
Future<void> handleUploadSuccess(MediaUpload media) async {
Log.info('Upload of ${media.mediaUploadId} success!');
currentUploadTasks.remove(media.mediaUploadId);
await twonlyDB.mediaUploadsDao.updateMediaUpload(
media.mediaUploadId,
MediaUploadsCompanion(
const MediaUploadsCompanion(
state: Value(UploadState.receiverNotified),
),
);
@ -488,7 +490,7 @@ Future handleUploadSuccess(MediaUpload media) async {
for (final messageId in media.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId(
messageId,
MessagesCompanion(
const MessagesCompanion(
acknowledgeByServer: Value(true),
errorWhileSending: Value(false),
),
@ -496,13 +498,13 @@ Future handleUploadSuccess(MediaUpload media) async {
}
}
Future handleUploadError(MediaUpload mediaUpload) async {
Future<void> handleUploadError(MediaUpload mediaUpload) async {
// if the messageIds are already there notify the user about this error...
if (mediaUpload.messageIds != null) {
for (int messageId in mediaUpload.messageIds!) {
for (final messageId in mediaUpload.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId(
messageId,
MessagesCompanion(
const MessagesCompanion(
errorWhileSending: Value(true),
),
);
@ -511,20 +513,20 @@ Future handleUploadError(MediaUpload mediaUpload) async {
await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId);
}
Future handleMediaUpload(MediaUpload media) async {
Uint8List bytesToUpload =
await readSendMediaFile(media.mediaUploadId, "encrypted");
Future<void> handleMediaUpload(MediaUpload media) async {
final bytesToUpload =
await readSendMediaFile(media.mediaUploadId, 'encrypted');
if (media.messageIds == null) return;
List<int> messageIds = media.messageIds!;
final messageIds = media.messageIds!;
List<Uint8List> downloadTokens = [];
final downloadTokens = <Uint8List>[];
List<TextMessage> messagesOnSuccess = [];
final messagesOnSuccess = <TextMessage>[];
for (var i = 0; i < messageIds.length; i++) {
Message? message = await twonlyDB.messagesDao
final message = await twonlyDB.messagesDao
.getMessageByMessageId(messageIds[i])
.getSingleOrNull();
if (message == null) continue;
@ -536,7 +538,7 @@ Future handleMediaUpload(MediaUpload media) async {
final downloadToken = createDownloadToken();
MessageJson msg = MessageJson(
final msg = MessageJson(
kind: MessageKind.media,
messageSenderId: messageIds[i],
content: MediaMessageContent(
@ -552,19 +554,19 @@ Future handleMediaUpload(MediaUpload media) async {
timestamp: media.metadata!.messageSendAt,
);
Uint8List plaintextContent =
final plaintextContent =
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
Contact? contact = await twonlyDB.contactsDao
final contact = await twonlyDB.contactsDao
.getContactByUserId(message.contactId)
.getSingleOrNull();
if (contact == null || contact.deleted) {
Log.warn(
"Contact deleted ${message.contactId} or not found in database.");
'Contact deleted ${message.contactId} or not found in database.');
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(errorWhileSending: Value(true)),
const MessagesCompanion(errorWhileSending: Value(true)),
);
continue;
}
@ -575,14 +577,14 @@ Future handleMediaUpload(MediaUpload media) async {
message.sendAt,
);
Uint8List? encryptedBytes = await signalEncryptMessage(
final encryptedBytes = await signalEncryptMessage(
message.contactId,
plaintextContent,
);
if (encryptedBytes == null) continue;
var messageOnSuccess = TextMessage()
final messageOnSuccess = TextMessage()
..body = encryptedBytes
..userId = Int64(message.contactId);
@ -615,29 +617,29 @@ Future handleMediaUpload(MediaUpload media) async {
final uploadRequestBytes = uploadRequest.writeToBuffer();
String? apiAuthTokenRaw =
await FlutterSecureStorage().read(key: SecureStorageKeys.apiAuthToken);
final apiAuthTokenRaw = await const FlutterSecureStorage()
.read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthTokenRaw == null) {
Log.error("api auth token not defined.");
Log.error('api auth token not defined.');
return;
}
String apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
File uploadRequestFile = await writeSendMediaFile(
final uploadRequestFile = await writeSendMediaFile(
media.mediaUploadId,
"upload",
'upload',
uploadRequestBytes,
);
String apiUrl =
"http${apiService.apiSecure}://${apiService.apiHost}/api/upload";
final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
try {
Log.info("Starting upload from ${media.mediaUploadId}");
Log.info('Starting upload from ${media.mediaUploadId}');
final task = UploadTask.fromFile(
taskId: "upload_${media.mediaUploadId}",
displayName: (media.metadata?.isVideo ?? false) ? "image" : "video",
taskId: 'upload_${media.mediaUploadId}',
displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video',
file: uploadRequestFile,
url: apiUrl,
priority: 0,
@ -652,62 +654,62 @@ Future handleMediaUpload(MediaUpload media) async {
try {
await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken);
} catch (e) {
Log.error("Fast upload failed: $e. Using slow method directly.");
enqueueUploadTask(media.mediaUploadId);
Log.error('Fast upload failed: $e. Using slow method directly.');
await enqueueUploadTask(media.mediaUploadId);
}
} catch (e) {
Log.error("Exception during upload: $e");
Log.error('Exception during upload: $e');
}
}
Map<int, UploadTask> currentUploadTasks = {};
Future enqueueUploadTask(int mediaUploadId) async {
Future<void> enqueueUploadTask(int mediaUploadId) async {
if (currentUploadTasks[mediaUploadId] == null) {
Log.info("could not enqueue upload task: $mediaUploadId");
Log.info('could not enqueue upload task: $mediaUploadId');
return;
}
Log.info("Enqueue upload task: $mediaUploadId");
Log.info('Enqueue upload task: $mediaUploadId');
await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!);
currentUploadTasks.remove(mediaUploadId);
await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId,
MediaUploadsCompanion(
const MediaUploadsCompanion(
state: Value(UploadState.uploadTaskStarted),
),
);
}
Future handleUploadWhenAppGoesBackground() async {
Future<void> handleUploadWhenAppGoesBackground() async {
if (currentUploadTasks.keys.isEmpty) {
return;
}
Log.info("App goes into background. Enqueue uploads to the background.");
Log.info('App goes into background. Enqueue uploads to the background.');
final keys = currentUploadTasks.keys.toList();
for (final key in keys) {
enqueueUploadTask(key);
}
}
Future uploadFileFast(
Future<void> uploadFileFast(
MediaUpload media,
Uint8List uploadRequestFile,
String apiUrl,
String apiAuthToken,
) async {
var requestMultipart = http.MultipartRequest(
"POST",
final requestMultipart = http.MultipartRequest(
'POST',
Uri.parse(apiUrl),
);
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
requestMultipart.files.add(http.MultipartFile.fromBytes(
"file",
'file',
uploadRequestFile,
filename: "upload",
filename: 'upload',
));
final response = await requestMultipart.send();
@ -721,9 +723,9 @@ Future uploadFileFast(
}
Future<bool> compressVideoIfExists(int mediaUploadId) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
File videoOriginalFile = File("$basePath.original.mp4");
File videoCompressedFile = File("$basePath.mp4");
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final videoOriginalFile = File('$basePath.original.mp4');
final videoCompressedFile = File('$basePath.mp4');
if (videoCompressedFile.existsSync()) {
// file is already compressed and exists
@ -735,38 +737,35 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
return false;
}
Stopwatch stopwatch = Stopwatch();
stopwatch.start();
final stopwatch = Stopwatch()..start();
MediaInfo? mediaInfo;
try {
mediaInfo = await VideoCompress.compressVideo(
videoOriginalFile.path,
quality: VideoQuality.Res1280x720Quality,
deleteOrigin: false,
includeAudio:
true, // https://github.com/jonataslaw/VideoCompress/issues/184
);
Log.info("Video has now size of ${mediaInfo!.filesize} bytes.");
Log.info('Video has now size of ${mediaInfo!.filesize} bytes.');
if (mediaInfo.filesize! >= 30 * 1000 * 1000) {
// if the media file is over 20MB compress it with low quality
mediaInfo = await VideoCompress.compressVideo(
videoOriginalFile.path,
quality: VideoQuality.Res960x540Quality,
deleteOrigin: false,
includeAudio: true,
);
}
} catch (e) {
Log.error("during video compression: $e");
Log.error('during video compression: $e');
}
stopwatch.stop();
Log.info("It took ${stopwatch.elapsedMilliseconds}ms to compress the video");
Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video');
if (mediaInfo == null) {
Log.error("could not compress video.");
Log.error('could not compress video.');
// as a fall back use the non compressed version
await videoOriginalFile.copy(videoCompressedFile.path);
await videoOriginalFile.delete();
@ -780,26 +779,26 @@ Future<bool> compressVideoIfExists(int mediaUploadId) async {
/// --- helper functions ---
Future<Uint8List> readSendMediaFile(int mediaUploadId, String type) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
File file = File("$basePath.$type");
if (!await file.exists()) {
throw Exception("$file not found");
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type');
if (!file.existsSync()) {
throw Exception('$file not found');
}
return await file.readAsBytes();
return file.readAsBytes();
}
Future<File> writeSendMediaFile(
int mediaUploadId, String type, Uint8List data) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
File file = File("$basePath.$type");
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type');
await file.writeAsBytes(data);
return file;
}
Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
File file = File("$basePath.$type");
if (await file.exists()) {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type');
if (file.existsSync()) {
await file.delete();
}
}
@ -807,7 +806,7 @@ Future<void> deleteSendMediaFile(int mediaUploadId, String type) async {
Future<String> getMediaFilePath(dynamic mediaId, String type) async {
final basedir = await getApplicationSupportDirectory();
final mediaSendDir = Directory(join(basedir.path, 'media', type));
if (!await mediaSendDir.exists()) {
if (!mediaSendDir.existsSync()) {
await mediaSendDir.create(recursive: true);
}
return join(mediaSendDir.path, '$mediaId');
@ -816,7 +815,7 @@ Future<String> getMediaFilePath(dynamic mediaId, String type) async {
Future<String> getMediaBaseFilePath(String type) async {
final basedir = await getApplicationSupportDirectory();
final mediaSendDir = Directory(join(basedir.path, 'media', type));
if (!await mediaSendDir.exists()) {
if (!mediaSendDir.existsSync()) {
await mediaSendDir.create(recursive: true);
}
return mediaSendDir.path;
@ -825,18 +824,15 @@ Future<String> getMediaBaseFilePath(String type) async {
/// combines two utf8 list
Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) {
final combinedLength = 4 + list1.length + list2.length;
final combinedList = Uint8List(combinedLength);
final byteData = ByteData.sublistView(combinedList);
byteData.setInt32(
0, list1.length, Endian.big); // Store size in big-endian format
combinedList.setRange(4, 4 + list1.length, list1);
combinedList.setRange(4 + list1.length, combinedLength, list2);
final combinedList = Uint8List(combinedLength)
..setRange(4, 4 + list1.length, list1)
..setRange(4 + list1.length, combinedLength, list2);
return combinedList;
}
List<Uint8List> extractUint8Lists(Uint8List combinedList) {
final byteData = ByteData.sublistView(combinedList);
final sizeOfList1 = byteData.getInt32(0, Endian.big);
final sizeOfList1 = byteData.getInt32(0);
final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1);
final list2 = Uint8List.view(combinedList.buffer, 4 + sizeOfList1,
combinedList.lengthInBytes - 4 - sizeOfList1);
@ -845,7 +841,7 @@ List<Uint8List> extractUint8Lists(Uint8List combinedList) {
Future<void> purgeSendMediaFiles() async {
final basedir = await getApplicationSupportDirectory();
final directory = Directory(join(basedir.path, 'media', "send"));
final directory = Directory(join(basedir.path, 'media', 'send'));
await purgeMediaFiles(directory);
}
@ -858,10 +854,10 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList(List<int>.generate(
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)));
Uint8List createDownloadToken() {
final Random random = Random();
final random = Random();
Uint8List token = Uint8List(32);
for (int j = 0; j < 32; j++) {
final token = Uint8List(32);
for (var j = 0; j < 32; j++) {
token[j] = random.nextInt(256); // Generate a random byte (0-255)
}
return token;

View file

@ -1,20 +1,19 @@
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:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
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/services/api/utils.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';
@ -22,12 +21,12 @@ import 'package:twonly/src/utils/storage.dart';
final lockRetransmission = Mutex();
Future tryTransmitMessages() async {
return await lockRetransmission.protect(() async {
Future<void> tryTransmitMessages() async {
return lockRetransmission.protect(() async {
final retransIds =
await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
Log.info("Retransmitting ${retransIds.length} text messages");
Log.info('Retransmitting ${retransIds.length} text messages');
if (retransIds.isEmpty) return;
@ -37,53 +36,51 @@ Future tryTransmitMessages() async {
});
}
Future sendRetransmitMessage(int retransId) async {
Future<void> sendRetransmitMessage(int retransId) async {
try {
MessageRetransmission? retrans = await twonlyDB.messageRetransmissionDao
final retrans = await twonlyDB.messageRetransmissionDao
.getRetransmissionById(retransId)
.getSingleOrNull();
if (retrans == null) {
Log.error("$retransId not found in database");
Log.error('$retransId not found in database');
return;
}
if (retrans.acknowledgeByServerAt != null) {
Log.error("$retransId message already retransmitted");
Log.error('$retransId message already retransmitted');
return;
}
MessageJson json = MessageJson.fromJson(
jsonDecode(
utf8.decode(
gzip.decode(retrans.plaintextContent),
),
final json = MessageJson.fromJson(jsonDecode(
utf8.decode(
gzip.decode(retrans.plaintextContent),
),
);
) as Map<String, dynamic>);
Log.info("Retransmitting $retransId: ${json.kind} to ${retrans.contactId}");
Log.info('Retransmitting $retransId: ${json.kind} to ${retrans.contactId}');
Contact? contact = await twonlyDB.contactsDao
final contact = await twonlyDB.contactsDao
.getContactByUserId(retrans.contactId)
.getSingleOrNull();
if (contact == null || contact.deleted) {
Log.warn("Contact deleted $retransId or not found in database.");
Log.warn('Contact deleted $retransId or not found in database.');
if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!,
MessagesCompanion(errorWhileSending: Value(true)),
const MessagesCompanion(errorWhileSending: Value(true)),
);
}
return;
}
Uint8List? encryptedBytes = await signalEncryptMessage(
final encryptedBytes = await signalEncryptMessage(
retrans.contactId,
retrans.plaintextContent,
);
if (encryptedBytes == null) {
Log.error("Could not encrypt the message. Aborting and trying again.");
Log.error('Could not encrypt the message. Aborting and trying again.');
return;
}
@ -96,27 +93,27 @@ Future sendRetransmitMessage(int retransId) async {
),
);
Result resp = await apiService.sendTextMessage(
final resp = await apiService.sendTextMessage(
retrans.contactId,
encryptedBytes,
retrans.pushData,
);
bool retry = true;
var retry = true;
if (resp.isError) {
Log.error("Could not retransmit message.");
Log.error('Could not retransmit message.');
if (resp.error == ErrorCode.UserIdNotFound) {
retry = false;
if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!,
MessagesCompanion(errorWhileSending: Value(true)),
const MessagesCompanion(errorWhileSending: Value(true)),
);
}
await twonlyDB.contactsDao.updateContact(
retrans.contactId,
ContactsCompanion(deleted: Value(true)),
const ContactsCompanion(deleted: Value(true)),
);
}
}
@ -126,7 +123,7 @@ Future sendRetransmitMessage(int retransId) async {
if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId(
retrans.messageId!,
MessagesCompanion(
const MessagesCompanion(
acknowledgeByServer: Value(true),
errorWhileSending: Value(false),
),
@ -148,13 +145,13 @@ Future sendRetransmitMessage(int retransId) async {
}
}
} catch (e) {
Log.error("error resending message: $e");
Log.error('error resending message: $e');
await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
}
}
// encrypts and stores the message and then sends it in the background
Future encryptAndSendMessageAsync(
Future<void> encryptAndSendMessageAsync(
int? messageId,
int userId,
MessageJson msg, {
@ -169,7 +166,8 @@ Future encryptAndSendMessageAsync(
pushData = await getPushData(userId, pushNotification);
}
int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission(
final retransId =
await twonlyDB.messageRetransmissionDao.insertRetransmission(
MessageRetransmissionsCompanion(
contactId: Value(userId),
messageId: Value(messageId),
@ -179,13 +177,13 @@ Future encryptAndSendMessageAsync(
);
if (retransId == null) {
Log.error("Could not insert the message into the retransmission database");
Log.error('Could not insert the message into the retransmission database');
return;
}
msg.retransId = retransId;
Uint8List plaintextContent =
final plaintextContent =
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
await twonlyDB.messageRetransmissionDao.updateRetransmission(
@ -194,29 +192,29 @@ Future encryptAndSendMessageAsync(
plaintextContent: Value(plaintextContent)));
// this can now be done in the background...
sendRetransmitMessage(retransId);
unawaited(sendRetransmitMessage(retransId));
}
Future sendTextMessage(
Future<void> sendTextMessage(
int target,
TextMessageContent content,
PushNotification? pushNotification,
) async {
DateTime messageSendAt = DateTime.now();
final messageSendAt = DateTime.now();
DateTime? openedAt;
if (pushNotification != null && pushNotification.hasReactionContent()) {
openedAt = DateTime.now();
}
int? messageId = await twonlyDB.messagesDao.insertMessage(
final messageId = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(target),
kind: Value(MessageKind.textMessage),
kind: const Value(MessageKind.textMessage),
sendAt: Value(messageSendAt),
responseToOtherMessageId: Value(content.responseToMessageId),
responseToMessageId: Value(content.responseToOtherMessageId),
downloadState: Value(DownloadState.downloaded),
downloadState: const Value(DownloadState.downloaded),
openedAt: Value(openedAt),
contentJson: Value(
jsonEncode(content.toJson()),
@ -230,7 +228,7 @@ Future sendTextMessage(
pushNotification.messageId = Int64(messageId);
}
MessageJson msg = MessageJson(
final msg = MessageJson(
kind: MessageKind.textMessage,
messageSenderId: messageId,
content: content,
@ -245,11 +243,11 @@ Future sendTextMessage(
);
}
Future notifyContactAboutOpeningMessage(
Future<void> notifyContactAboutOpeningMessage(
int fromUserId,
List<int> messageOtherIds,
) async {
int biggestMessageId = messageOtherIds.first;
var biggestMessageId = messageOtherIds.first;
for (final messageOtherId in messageOtherIds) {
if (messageOtherId > biggestMessageId) biggestMessageId = messageOtherId;
@ -267,17 +265,16 @@ Future notifyContactAboutOpeningMessage(
await updateLastMessageId(fromUserId, biggestMessageId);
}
Future notifyContactsAboutProfileChange() async {
List<Contact> contacts =
await twonlyDB.contactsDao.getAllNotBlockedContacts();
Future<void> notifyContactsAboutProfileChange() async {
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
UserData? user = await getUser();
final user = await getUser();
if (user == null) return;
if (user.avatarSvg == null) return;
for (Contact contact in contacts) {
for (final contact in contacts) {
if (contact.myAvatarCounter < user.avatarCounter) {
twonlyDB.contactsDao.updateContact(
await twonlyDB.contactsDao.updateContact(
contact.userId,
ContactsCompanion(
myAvatarCounter: Value(user.avatarCounter),

View file

@ -1,14 +1,15 @@
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/media_uploads_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client;
@ -16,10 +17,10 @@ import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/api/media_upload.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/api/media_download.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
@ -31,7 +32,7 @@ import 'package:twonly/src/views/components/animate_icon.dart';
final lockHandleServerMessage = Mutex();
Future handleServerMessage(server.ServerToClient msg) async {
Future<void> handleServerMessage(server.ServerToClient msg) async {
return lockHandleServerMessage.protect(() async {
client.Response? response;
@ -39,34 +40,35 @@ Future handleServerMessage(server.ServerToClient msg) async {
if (msg.v0.hasRequestNewPreKeys()) {
response = await handleRequestNewPreKey();
} else if (msg.v0.hasNewMessage()) {
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
int fromUserId = msg.v0.newMessage.fromUserId.toInt();
final body = Uint8List.fromList(msg.v0.newMessage.body);
final fromUserId = msg.v0.newMessage.fromUserId.toInt();
response = await handleNewMessage(fromUserId, body);
} else {
Log.error("Got a new message from the server: $msg");
Log.error('Got a new message from the server: $msg');
response = client.Response()..error = ErrorCode.InternalError;
}
} catch (e) {
response = client.Response()..error = ErrorCode.InternalError;
}
var v0 = client.V0()
final v0 = client.V0()
..seq = msg.v0.seq
..response = response;
apiService.sendResponse(ClientToServer()..v0 = v0);
await apiService.sendResponse(ClientToServer()..v0 = v0);
});
}
DateTime lastSignalDecryptMessage = DateTime.now().subtract(Duration(hours: 1));
DateTime lastPushKeyRequest = DateTime.now().subtract(Duration(hours: 1));
DateTime lastSignalDecryptMessage =
DateTime.now().subtract(const Duration(hours: 1));
DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1));
bool messageGetsAck(MessageKind kind) {
return kind != MessageKind.pushKey && kind != MessageKind.ack;
}
Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
MessageJson? message = await signalDecryptMessage(fromUserId, body);
final message = await signalDecryptMessage(fromUserId, body);
if (message == null) {
final encryptedHash = (await Sha256().hash(body)).bytes;
await encryptAndSendMessageAsync(
@ -79,17 +81,17 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
),
);
Log.error("Could not decrypt others message!");
Log.error('Could not decrypt others message!');
// Message is not valid, so server can delete it
var ok = client.Response_Ok()..none = true;
final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
Log.info("Got: ${message.kind} from $fromUserId");
Log.info('Got: ${message.kind} from $fromUserId');
if (messageGetsAck(message.kind) && message.retransId != null) {
Log.info("Sending ACK for ${message.kind}");
Log.info('Sending ACK for ${message.kind}');
/// ACK every message
await encryptAndSendMessageAsync(
@ -111,7 +113,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
final content = message.content;
if (content is AckContent) {
if (content.messageIdToAck != null) {
final update = MessagesCompanion(
const update = MessagesCompanion(
acknowledgeByUser: Value(true),
errorWhileSending: Value(false),
);
@ -125,10 +127,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
await twonlyDB.messageRetransmissionDao
.deleteRetransmissionById(content.retransIdToAck);
}
break;
case MessageKind.signalDecryptError:
Log.error(
"Got signal decrypt error from other user! Sending all non ACK messages again.");
'Got signal decrypt error from other user! Sending all non ACK messages again.');
final content = message.content;
if (content is SignalDecryptErrorContent) {
@ -140,16 +141,15 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
final message = await twonlyDB.messageRetransmissionDao
.getRetransmissionFromHash(fromUserId, hash);
if (message != null) {
sendRetransmitMessage(message.retransmissionId);
unawaited(sendRetransmitMessage(message.retransmissionId));
}
}
break;
case MessageKind.contactRequest:
return handleContactRequest(fromUserId, message);
case MessageKind.flameSync:
Contact? contact = await twonlyDB.contactsDao
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null && contact.lastFlameCounterChange != null) {
@ -188,12 +188,12 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
openedMessage.mediaRetransmissionState ==
MediaRetransmitting.none &&
openedMessage.sendAt
.isAfter(DateTime.now().subtract(Duration(days: 2)))) {
.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
twonlyDB.mediaUploadsDao.updateMediaUpload(
await twonlyDB.mediaUploadsDao.updateMediaUpload(
openedMessage.mediaUploadId!,
MediaUploadsCompanion(
const MediaUploadsCompanion(
state: Value(
UploadState.pending,
),
@ -203,18 +203,18 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageReceiverId!,
MessagesCompanion(
const MessagesCompanion(
downloadState: Value(DownloadState.pending),
mediaRetransmissionState:
Value(MediaRetransmitting.retransmitted),
),
);
retryMediaUpload(false);
unawaited(retryMediaUpload(false));
} else {
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageReceiverId!,
MessagesCompanion(
const MessagesCompanion(
errorWhileSending: Value(true),
),
);
@ -226,7 +226,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
if (message.messageReceiverId != null) {
final update = MessagesCompanion(
openedAt: Value(message.timestamp),
errorWhileSending: Value(false),
errorWhileSending: const Value(false),
);
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
@ -243,20 +243,17 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
);
}
}
break;
case MessageKind.rejectRequest:
await deleteContact(fromUserId);
break;
case MessageKind.acceptRequest:
final update = ContactsCompanion(accepted: Value(true));
const update = ContactsCompanion(accepted: Value(true));
await twonlyDB.contactsDao.updateContact(fromUserId, update);
notifyContactsAboutProfileChange();
break;
unawaited(notifyContactsAboutProfileChange());
case MessageKind.profileChange:
var content = message.content;
final content = message.content;
if (content is ProfileContent) {
final update = ContactsCompanion(
avatarSvg: Value(content.avatarSvg),
@ -264,14 +261,13 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
);
await twonlyDB.contactsDao.updateContact(fromUserId, update);
}
createPushAvatars();
break;
unawaited(createPushAvatars());
case MessageKind.requestPushKey:
if (lastPushKeyRequest
.isBefore(DateTime.now().subtract(Duration(seconds: 60)))) {
.isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) {
lastPushKeyRequest = DateTime.now();
setupNotificationWithUsers(forceContact: fromUserId);
unawaited(setupNotificationWithUsers(forceContact: fromUserId));
}
case MessageKind.pushKey:
@ -282,14 +278,15 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
}
}
// ignore: no_default_cases
default:
if (message.kind != MessageKind.textMessage &&
message.kind != MessageKind.media &&
message.kind != MessageKind.storedMediaFile &&
message.kind != MessageKind.reopenedMedia) {
Log.error("Got unknown MessageKind $message");
Log.error('Got unknown MessageKind $message');
} else if (message.messageSenderId == null) {
Log.error("Messageid not defined $message");
Log.error('Messageid not defined $message');
} else {
if (message.kind == MessageKind.storedMediaFile) {
if (message.messageReceiverId != null) {
@ -297,7 +294,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageReceiverId!,
MessagesCompanion(
const MessagesCompanion(
mediaStored: Value(true),
errorWhileSending: Value(false),
),
@ -308,11 +305,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
.getSingleOrNull();
if (msg != null && msg.mediaUploadId != null) {
final filePath =
await getMediaFilePath(msg.mediaUploadId, "send");
if (filePath.contains("mp4")) {
createThumbnailsForVideo(File(filePath));
await getMediaFilePath(msg.mediaUploadId, 'send');
if (filePath.contains('mp4')) {
unawaited(createThumbnailsForVideo(File(filePath)));
} else {
createThumbnailsForImage(File(filePath));
unawaited(createThumbnailsForImage(File(filePath)));
}
}
}
@ -330,8 +327,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
.deleteMessagesByMessageId(openedMessage.messageId);
} else {
Log.error(
"Got a duplicated message from other user: ${message.messageSenderId!}");
var ok = client.Response_Ok()..none = true;
'Got a duplicated message from other user: ${message.messageSenderId!}');
final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
}
@ -340,7 +337,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
int? responseToOtherMessageId;
int? messageId;
bool acknowledgeByUser = false;
var acknowledgeByUser = false;
DateTime? openedAt;
if (message.kind == MessageKind.reopenedMedia) {
@ -369,7 +366,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
fromUserId,
responseToMessageId,
MessagesCompanion(
errorWhileSending: Value(false),
errorWhileSending: const Value(false),
openedAt: Value(
DateTime.now(),
), // when a user reacted to the media file, it should be marked as opened
@ -377,13 +374,13 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
);
}
String contentJson = jsonEncode(content.toJson());
final contentJson = jsonEncode(content.toJson());
final update = MessagesCompanion(
contactId: Value(fromUserId),
kind: Value(message.kind),
messageOtherId: Value(message.messageSenderId),
contentJson: Value(contentJson),
acknowledgeByServer: Value(true),
acknowledgeByServer: const Value(true),
acknowledgeByUser: Value(acknowledgeByUser),
responseToMessageId: Value(responseToMessageId),
responseToOtherMessageId: Value(responseToOtherMessageId),
@ -403,7 +400,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
}
if (message.kind == MessageKind.media) {
twonlyDB.contactsDao.incFlameCounter(
await twonlyDB.contactsDao.incFlameCounter(
fromUserId,
true,
message.timestamp,
@ -413,37 +410,37 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
.getMessageByMessageId(messageId)
.getSingleOrNull();
if (msg != null) {
startDownloadMedia(msg, false);
unawaited(startDownloadMedia(msg, false));
}
}
} else {
Log.error("Content is not defined $message");
Log.error('Content is not defined $message');
}
// unarchive contact when receiving a new message
await twonlyDB.contactsDao.updateContact(
fromUserId,
ContactsCompanion(
const ContactsCompanion(
archived: Value(false),
),
);
}
}
var ok = client.Response_Ok()..none = true;
final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
Future<client.Response> handleRequestNewPreKey() async {
List<PreKeyRecord> localPreKeys = await signalGetPreKeys();
final localPreKeys = await signalGetPreKeys();
List<client.Response_PreKey> prekeysList = [];
for (int i = 0; i < localPreKeys.length; i++) {
final prekeysList = <client.Response_PreKey>[];
for (var i = 0; i < localPreKeys.length; i++) {
prekeysList.add(client.Response_PreKey()
..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
}
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
var ok = client.Response_Ok()..prekeys = prekeys;
final prekeys = client.Response_Prekeys(prekeys: prekeysList);
final ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok;
}
@ -451,18 +448,18 @@ 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 apiService.getUsername(fromUserId);
final username = await apiService.getUsername(fromUserId);
if (username.isSuccess) {
Uint8List name = username.value.userdata.username;
final name = username.value.userdata.username as Uint8List;
await twonlyDB.contactsDao.insertContact(
ContactsCompanion(
username: Value(utf8.decode(name)),
userId: Value(fromUserId),
requested: Value(true),
requested: const Value(true),
),
);
}
await setupNotificationWithUsers();
var ok = client.Response_Ok()..none = true;
final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}

View file

@ -14,16 +14,17 @@ import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
class Result<T, E> {
Result.error(this.error) : value = null;
Result.success(this.value) : error = null;
final T? value;
final E? error;
bool get isSuccess => value != null;
bool get isError => error != null;
Result.success(this.value) : error = null;
Result.error(this.error) : value = null;
}
// ignore: strict_raw_type
Result asResult(server.ServerToClient? msg) {
if (msg == null) {
return Result.error(ErrorCode.InternalError);
@ -44,20 +45,20 @@ ClientToServer createClientToServerFromHandshake(Handshake handshake) {
ClientToServer createClientToServerFromApplicationData(
ApplicationData applicationData) {
var v0 = client.V0()
final v0 = client.V0()
..seq = Int64(0)
..applicationdata = applicationData;
return ClientToServer()..v0 = v0;
}
Future deleteContact(int contactId) async {
Future<void> deleteContact(int contactId) async {
await twonlyDB.messagesDao.deleteAllMessagesByContactId(contactId);
await twonlyDB.signalDao.deleteAllByContactId(contactId);
await deleteSessionWithTarget(contactId);
await twonlyDB.contactsDao.deleteContactByUserId(contactId);
}
Future rejectUser(int contactId) async {
Future<void> rejectUser(int contactId) async {
await encryptAndSendMessageAsync(
null,
contactId,
@ -69,10 +70,10 @@ Future rejectUser(int contactId) async {
);
}
Future handleMediaError(Message message) async {
Future<void> handleMediaError(Message message) async {
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(
const MessagesCompanion(
errorWhileSending: Value(true),
mediaRetransmissionState: Value(
MediaRetransmitting.requested,
@ -80,7 +81,7 @@ Future handleMediaError(Message message) async {
),
);
if (message.messageOtherId != null) {
encryptAndSendMessageAsync(
await encryptAndSendMessageAsync(
null,
message.contactId,
MessageJson(

View file

@ -1,3 +1,5 @@
import 'dart:io' show Platform;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@ -5,22 +7,22 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/log.dart';
import 'dart:io' show Platform;
import '../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future initFCMAfterAuthenticated() async {
Future<void> initFCMAfterAuthenticated() async {
if (globalIsAppInBackground) return;
final storage = FlutterSecureStorage();
const storage = FlutterSecureStorage();
String? storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
try {
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) {
Log.error("Error getting fcmToken");
Log.error('Error getting fcmToken');
return;
}
@ -33,14 +35,14 @@ Future initFCMAfterAuthenticated() async {
await apiService.updateFCMToken(fcmToken);
await storage.write(key: SecureStorageKeys.googleFcm, value: fcmToken);
}).onError((err) {
Log.error("could not listen on token refresh");
Log.error('could not listen on token refresh');
});
} catch (e) {
Log.error("could not load fcm token: $e");
Log.error('could not load fcm token: $e');
}
}
Future initFCMService() async {
Future<void> initFCMService() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
@ -51,15 +53,7 @@ Future initFCMService() async {
// of notifications they would like to receive once the user receives a notification.
// final notificationSettings =
// await FirebaseMessaging.instance.requestPermission(provisional: true);
await FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
await FirebaseMessaging.instance.requestPermission();
// For apple platforms, ensure the APNS token is available before making any FCM plugin API calls
if (Platform.isIOS) {
@ -69,9 +63,7 @@ Future initFCMService() async {
}
}
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
handleRemoteMessage(message);
});
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
}
@pragma('vm:entry-point')
@ -80,19 +72,19 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message);
// make sure every thing run...
await Future.delayed(Duration(milliseconds: 2000));
await Future.delayed(const Duration(milliseconds: 2000));
}
Future handleRemoteMessage(RemoteMessage message) async {
Future<void> handleRemoteMessage(RemoteMessage message) async {
if (!Platform.isAndroid) {
Log.error("Got message in Dart while on iOS");
Log.error('Got message in Dart while on iOS');
}
if (message.notification != null) {
String title = message.notification!.title ?? "";
String body = message.notification!.body ?? "";
final title = message.notification!.title ?? '';
final body = message.notification!.body ?? '';
await customLocalPushNotification(title, body);
} else if (message.data["push_data"] != null) {
await handlePushData(message.data["push_data"]);
} else if (message.data['push_data'] != null) {
await handlePushData(message.data['push_data'] as String);
}
}

View file

@ -9,7 +9,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/model/json/message.dart' as my;
Future syncFlameCounters() async {
Future<void> syncFlameCounters() async {
var user = await getUser();
if (user == null) return;

View file

@ -14,9 +14,8 @@ import 'package:twonly/src/utils/log.dart';
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Future customLocalPushNotification(String title, String msg) async {
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
Future<void> customLocalPushNotification(String title, String msg) async {
const androidNotificationDetails = AndroidNotificationDetails(
'1',
'System',
channelDescription: 'System messages.',
@ -24,9 +23,8 @@ Future customLocalPushNotification(String title, String msg) async {
priority: Priority.max,
);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
const NotificationDetails notificationDetails = NotificationDetails(
const darwinNotificationDetails = DarwinNotificationDetails();
const notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
@ -39,7 +37,7 @@ Future customLocalPushNotification(String title, String msg) async {
);
}
Future handlePushData(String pushDataB64) async {
Future<void> handlePushData(String pushDataB64) async {
try {
final pushData =
EncryptedPushNotification.fromBuffer(base64.decode(pushDataB64));
@ -48,7 +46,7 @@ Future handlePushData(String pushDataB64) async {
PushUser? foundPushUser;
if (pushData.keyId == 0) {
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
final key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
pushNotification = await tryDecryptMessage(key, pushData);
} else {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
@ -70,14 +68,14 @@ Future handlePushData(String pushDataB64) async {
if (pushNotification != null) {
if (pushNotification.kind == PushKind.testNotification) {
await customLocalPushNotification(
"Test notification",
"This is a test notification.",
'Test notification',
'This is a test notification.',
);
} else if (foundPushUser != null) {
if (pushNotification.hasMessageId()) {
if (pushNotification.messageId <= foundPushUser.lastMessageId) {
Log.info(
"Got a push notification for a message which was already opened.",
'Got a push notification for a message which was already opened.',
);
return;
}
@ -90,8 +88,8 @@ Future handlePushData(String pushDataB64) async {
}
} catch (e) {
await customLocalPushNotification(
"Du hast eine neue Nachricht.",
"Öffne twonly um mehr zu erfahren.",
'Du hast eine neue Nachricht.',
'Öffne twonly um mehr zu erfahren.',
);
Log.error(e);
}
@ -101,9 +99,9 @@ Future<PushNotification?> tryDecryptMessage(
List<int> key, EncryptedPushNotification push) async {
try {
final chacha20 = FlutterChacha20.poly1305Aead();
SecretKeyData secretKeyData = SecretKeyData(key);
final secretKeyData = SecretKeyData(key);
SecretBox secretBox = SecretBox(
final secretBox = SecretBox(
push.ciphertext,
nonce: push.nonce,
mac: Mac(push.mac),
@ -118,7 +116,7 @@ Future<PushNotification?> tryDecryptMessage(
}
}
Future showLocalPushNotification(
Future<void> showLocalPushNotification(
PushUser pushUser,
PushNotification pushNotification,
) async {
@ -127,24 +125,23 @@ Future showLocalPushNotification(
// do not show notification for blocked users...
if (pushUser.blocked) {
Log.info("Blocked a message from a blocked user!");
Log.info('Blocked a message from a blocked user!');
return;
}
title = pushUser.displayName;
body = getPushNotificationText(pushNotification);
if (body == "") {
Log.error("No push notification type defined!");
if (body == '') {
Log.error('No push notification type defined!');
}
FilePathAndroidBitmap? styleInformation;
String? avatarPath = await getAvatarIcon(pushUser.userId.toInt());
final avatarPath = await getAvatarIcon(pushUser.userId.toInt());
if (avatarPath != null) {
styleInformation = FilePathAndroidBitmap(avatarPath);
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
final androidNotificationDetails = AndroidNotificationDetails(
'0',
'Messages',
channelDescription: 'Messages from other users.',
@ -154,9 +151,8 @@ Future showLocalPushNotification(
largeIcon: styleInformation,
);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails(
const darwinNotificationDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
@ -170,19 +166,18 @@ Future showLocalPushNotification(
);
}
Future showLocalPushNotificationWithoutUserId(
Future<void> showLocalPushNotificationWithoutUserId(
PushNotification pushNotification,
) async {
String? title;
String? body;
body = getPushNotificationTextWithoutUserId(pushNotification.kind);
if (body == "") {
Log.error("No push notification type defined!");
if (body == '') {
Log.error('No push notification type defined!');
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
const androidNotificationDetails = AndroidNotificationDetails(
'0',
'Messages',
channelDescription: 'Messages from other users.',
@ -191,9 +186,8 @@ Future showLocalPushNotificationWithoutUserId(
ticker: 'You got a new message.',
);
const DarwinNotificationDetails darwinNotificationDetails =
DarwinNotificationDetails();
NotificationDetails notificationDetails = NotificationDetails(
const darwinNotificationDetails = DarwinNotificationDetails();
const notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails);
await flutterLocalNotificationsPlugin.show(
@ -219,99 +213,99 @@ Future<String?> getAvatarIcon(int contactId) async {
String getPushNotificationTextWithoutUserId(PushKind pushKind) {
Map<String, String> pushNotificationText;
String systemLanguage = Platform.localeName;
final systemLanguage = Platform.localeName;
if (systemLanguage.contains("de")) {
if (systemLanguage.contains('de')) {
pushNotificationText = {
PushKind.text.name: "Du hast eine neue Nachricht erhalten.",
PushKind.twonly.name: "Du hast ein neues twonly erhalten.",
PushKind.video.name: "Du hast ein neues Video erhalten.",
PushKind.image.name: "Du hast ein neues Bild erhalten.",
PushKind.text.name: 'Du hast eine neue Nachricht erhalten.',
PushKind.twonly.name: 'Du hast ein neues twonly erhalten.',
PushKind.video.name: 'Du hast ein neues Video erhalten.',
PushKind.image.name: 'Du hast ein neues Bild erhalten.',
PushKind.contactRequest.name:
"Du hast eine neue Kontaktanfrage erhalten.",
PushKind.acceptRequest.name: "Deine Kontaktanfrage wurde angenommen.",
PushKind.storedMediaFile.name: "Dein Bild wurde gespeichert.",
PushKind.reaction.name: "Du hast eine Reaktion auf dein Bild erhalten.",
PushKind.reopenedMedia.name: "Dein Bild wurde erneut geöffnet.",
'Du hast eine neue Kontaktanfrage erhalten.',
PushKind.acceptRequest.name: 'Deine Kontaktanfrage wurde angenommen.',
PushKind.storedMediaFile.name: 'Dein Bild wurde gespeichert.',
PushKind.reaction.name: 'Du hast eine Reaktion auf dein Bild erhalten.',
PushKind.reopenedMedia.name: 'Dein Bild wurde erneut geöffnet.',
PushKind.reactionToVideo.name:
"Du hast eine Reaktion auf dein Video erhalten.",
'Du hast eine Reaktion auf dein Video erhalten.',
PushKind.reactionToText.name:
"Du hast eine Reaktion auf deinen Text erhalten.",
'Du hast eine Reaktion auf deinen Text erhalten.',
PushKind.reactionToImage.name:
"Du hast eine Reaktion auf dein Bild erhalten.",
PushKind.response.name: "Du hast eine Antwort erhalten.",
'Du hast eine Reaktion auf dein Bild erhalten.',
PushKind.response.name: 'Du hast eine Antwort erhalten.',
};
} else {
pushNotificationText = {
PushKind.text.name: "You have received a new message.",
PushKind.twonly.name: "You have received a new twonly.",
PushKind.video.name: "You have received a new video.",
PushKind.image.name: "You have received a new image.",
PushKind.contactRequest.name: "You have received a new contact request.",
PushKind.acceptRequest.name: "Your contact request has been accepted.",
PushKind.storedMediaFile.name: "Your image has been saved.",
PushKind.reaction.name: "You have received a reaction to your image.",
PushKind.reopenedMedia.name: "Your image has been reopened.",
PushKind.text.name: 'You have received a new message.',
PushKind.twonly.name: 'You have received a new twonly.',
PushKind.video.name: 'You have received a new video.',
PushKind.image.name: 'You have received a new image.',
PushKind.contactRequest.name: 'You have received a new contact request.',
PushKind.acceptRequest.name: 'Your contact request has been accepted.',
PushKind.storedMediaFile.name: 'Your image has been saved.',
PushKind.reaction.name: 'You have received a reaction to your image.',
PushKind.reopenedMedia.name: 'Your image has been reopened.',
PushKind.reactionToVideo.name:
"You have received a reaction to your video.",
'You have received a reaction to your video.',
PushKind.reactionToText.name:
"You have received a reaction to your text.",
'You have received a reaction to your text.',
PushKind.reactionToImage.name:
"You have received a reaction to your image.",
PushKind.response.name: "You have received a response.",
'You have received a reaction to your image.',
PushKind.response.name: 'You have received a response.',
};
}
return pushNotificationText[pushKind.name] ?? "";
return pushNotificationText[pushKind.name] ?? '';
}
String getPushNotificationText(PushNotification pushNotification) {
String systemLanguage = Platform.localeName;
final systemLanguage = Platform.localeName;
Map<String, String> pushNotificationText;
if (systemLanguage.contains("de")) {
if (systemLanguage.contains('de')) {
pushNotificationText = {
PushKind.text.name: "hat dir eine Nachricht gesendet.",
PushKind.twonly.name: "hat dir ein twonly gesendet.",
PushKind.video.name: "hat dir ein Video gesendet.",
PushKind.image.name: "hat dir ein Bild gesendet.",
PushKind.contactRequest.name: "möchte sich mit dir vernetzen.",
PushKind.acceptRequest.name: "ist jetzt mit dir vernetzt.",
PushKind.storedMediaFile.name: "hat dein Bild gespeichert.",
PushKind.reaction.name: "hat auf dein Bild reagiert.",
PushKind.reopenedMedia.name: "hat dein Bild erneut geöffnet.",
PushKind.text.name: 'hat dir eine Nachricht gesendet.',
PushKind.twonly.name: 'hat dir ein twonly gesendet.',
PushKind.video.name: 'hat dir ein Video gesendet.',
PushKind.image.name: 'hat dir ein Bild gesendet.',
PushKind.contactRequest.name: 'möchte sich mit dir vernetzen.',
PushKind.acceptRequest.name: 'ist jetzt mit dir vernetzt.',
PushKind.storedMediaFile.name: 'hat dein Bild gespeichert.',
PushKind.reaction.name: 'hat auf dein Bild reagiert.',
PushKind.reopenedMedia.name: 'hat dein Bild erneut geöffnet.',
PushKind.reactionToVideo.name:
"hat mit {{reaction}} auf dein Video reagiert.",
'hat mit {{reaction}} auf dein Video reagiert.',
PushKind.reactionToText.name:
"hat mit {{reaction}} auf deine Nachricht reagiert.",
'hat mit {{reaction}} auf deine Nachricht reagiert.',
PushKind.reactionToImage.name:
"hat mit {{reaction}} auf dein Bild reagiert.",
PushKind.response.name: "hat dir geantwortet.",
'hat mit {{reaction}} auf dein Bild reagiert.',
PushKind.response.name: 'hat dir geantwortet.',
};
} else {
pushNotificationText = {
PushKind.text.name: "has sent you a message.",
PushKind.twonly.name: "has sent you a twonly.",
PushKind.video.name: "has sent you a video.",
PushKind.image.name: "has sent you an image.",
PushKind.contactRequest.name: "wants to connect with you.",
PushKind.acceptRequest.name: "is now connected with you.",
PushKind.storedMediaFile.name: "has stored your image.",
PushKind.reaction.name: "has reacted to your image.",
PushKind.reopenedMedia.name: "has reopened your image.",
PushKind.text.name: 'has sent you a message.',
PushKind.twonly.name: 'has sent you a twonly.',
PushKind.video.name: 'has sent you a video.',
PushKind.image.name: 'has sent you an image.',
PushKind.contactRequest.name: 'wants to connect with you.',
PushKind.acceptRequest.name: 'is now connected with you.',
PushKind.storedMediaFile.name: 'has stored your image.',
PushKind.reaction.name: 'has reacted to your image.',
PushKind.reopenedMedia.name: 'has reopened your image.',
PushKind.reactionToVideo.name:
"has reacted with {{reaction}} to your video.",
'has reacted with {{reaction}} to your video.',
PushKind.reactionToText.name:
"has reacted with {{reaction}} to your message.",
'has reacted with {{reaction}} to your message.',
PushKind.reactionToImage.name:
"has reacted with {{reaction}} to your image.",
PushKind.response.name: "has responded.",
'has reacted with {{reaction}} to your image.',
PushKind.response.name: 'has responded.',
};
}
var contentText = pushNotificationText[pushNotification.kind.name] ?? "";
var contentText = pushNotificationText[pushNotification.kind.name] ?? '';
if (pushNotification.hasReactionContent()) {
contentText = contentText.replaceAll(
"{{reaction}}", pushNotification.reactionContent);
'{{reaction}}', pushNotification.reactionContent);
}
return contentText;
}

View file

@ -18,22 +18,22 @@ import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/log.dart';
/// This function must be called after the database is setup
Future setupNotificationWithUsers(
Future<void> setupNotificationWithUsers(
{bool force = false, int? forceContact}) async {
var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
// HotFIX: Search for user with id 0 if not there remove all
// and create new push keys with all users.
PushUser? pushUser = pushUsers.firstWhereOrNull((x) => x.userId == 0);
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == 0);
if (pushUser == null) {
Log.info("Clearing push keys");
Log.info('Clearing push keys');
await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
pushUsers.add(PushUser(
userId: Int64(0),
displayName: "NoUser",
pushKeys: [],
));
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys)
..add(PushUser(
userId: Int64(),
displayName: 'NoUser',
pushKeys: [],
));
}
var wasChanged = false;
@ -42,7 +42,7 @@ Future setupNotificationWithUsers(
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
for (final contact in contacts) {
PushUser? pushUser =
final pushUser =
pushUsers.firstWhereOrNull((x) => x.userId == contact.userId);
if (pushUser != null) {
@ -67,11 +67,11 @@ Future setupNotificationWithUsers(
pushUser.pushKeys.add(lastKey);
pushUser.pushKeys.add(pushKey);
wasChanged = true;
Log.info("Creating new pushkey for ${contact.userId}");
Log.info('Creating new pushkey for ${contact.userId}');
}
} else {
Log.info(
"User ${contact.userId} not yet in pushkeys. Creating a new user.",
'User ${contact.userId} not yet in pushkeys. Creating a new user.',
);
wasChanged = true;
@ -87,7 +87,6 @@ Future setupNotificationWithUsers(
displayName: getContactDisplayName(contact),
blocked: contact.blocked,
pushKeys: [pushKey],
lastMessageId: null,
));
}
}
@ -97,7 +96,7 @@ Future setupNotificationWithUsers(
}
}
Future sendNewPushKey(int userId, PushKey pushKey) async {
Future<void> sendNewPushKey(int userId, PushKey pushKey) async {
await encryptAndSendMessageAsync(
null,
userId,
@ -114,11 +113,10 @@ Future sendNewPushKey(int userId, PushKey pushKey) async {
);
}
Future updatePushUser(Contact contact) async {
var pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
Future<void> updatePushUser(Contact contact) async {
final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
PushUser? pushUser =
pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
if (pushUser == null) {
pushKeys.add(PushUser(
@ -126,20 +124,21 @@ Future updatePushUser(Contact contact) async {
displayName: getContactDisplayName(contact),
pushKeys: [],
blocked: contact.blocked,
lastMessageId: Int64(0),
lastMessageId: Int64(),
));
} else {
pushUser.displayName = getContactDisplayName(contact);
pushUser.blocked = contact.blocked;
pushUser
..displayName = getContactDisplayName(contact)
..blocked = contact.blocked;
}
await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys);
}
Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
var pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
Future<void> handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
PushUser? pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
var pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
if (pushUser == null) {
final contact = await twonlyDB.contactsDao
@ -151,13 +150,13 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
displayName: getContactDisplayName(contact),
pushKeys: [],
blocked: contact.blocked,
lastMessageId: Int64(0),
lastMessageId: Int64(),
));
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
}
if (pushUser == null) {
Log.error("could not store new push key as no user was found");
Log.error('could not store new push key as no user was found');
}
// only store the newest key...
@ -173,14 +172,12 @@ Future handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys);
}
Future updateLastMessageId(int fromUserId, int messageId) async {
List<PushUser> pushUsers =
await getPushKeys(SecureStorageKeys.receivingPushKeys);
Future<void> updateLastMessageId(int fromUserId, int messageId) async {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
PushUser? pushUser =
pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == fromUserId);
if (pushUser == null) {
setupNotificationWithUsers();
unawaited(setupNotificationWithUsers());
return;
}
@ -193,13 +190,12 @@ Future updateLastMessageId(int fromUserId, int messageId) async {
/// this will trigger a push notification
/// push notification only containing the message kind and username
Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
final List<PushUser> pushKeys =
await getPushKeys(SecureStorageKeys.sendingPushKeys);
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
int keyId = 0;
var key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
var keyId = 0;
PushUser? pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId);
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == toUserId);
if (pushUser == null) {
// user does not have send any push keys
@ -210,7 +206,7 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
content.kind != PushKind.testNotification) {
// 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!");
Log.error('Using insecure key as the receiver does not send a push key!');
await encryptAndSendMessageAsync(
null,
toUserId,
@ -226,7 +222,7 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
key = pushUser.pushKeys.last.key;
keyId = pushUser.pushKeys.last.id.toInt();
} catch (e) {
Log.error("No push notification key found for user $toUserId");
Log.error('No push notification key found for user $toUserId');
return null;
}
}
@ -248,39 +244,36 @@ Future<Uint8List?> getPushData(int toUserId, PushNotification content) async {
}
Future<List<PushUser>> getPushKeys(String storageKey) async {
var storage = FlutterSecureStorage();
String? pushKeysProto = await storage.read(
const storage = FlutterSecureStorage();
final pushKeysProto = await storage.read(
key: storageKey,
iOptions: IOSOptions(
groupId: "CN332ZUGRP.eu.twonly.shared",
synchronizable: false,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
if (pushKeysProto == null) return [];
Uint8List pushKeysRaw = base64Decode(pushKeysProto);
final pushKeysRaw = base64Decode(pushKeysProto);
return PushUsers.fromBuffer(pushKeysRaw).users;
}
Future setPushKeys(String storageKey, List<PushUser> pushKeys) async {
var storage = FlutterSecureStorage();
Future<void> setPushKeys(String storageKey, List<PushUser> pushKeys) async {
const storage = FlutterSecureStorage();
await storage.delete(
key: storageKey,
iOptions: IOSOptions(
groupId: "CN332ZUGRP.eu.twonly.shared",
synchronizable: false,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
String jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer());
final jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer());
await storage.write(
key: storageKey,
value: jsonString,
iOptions: IOSOptions(
groupId: "CN332ZUGRP.eu.twonly.shared",
synchronizable: false,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);

View file

@ -58,7 +58,7 @@ Future<void> setupPushNotification() async {
);
}
Future createPushAvatars() async {
Future<void> createPushAvatars() async {
if (!Platform.isAndroid) {
return; // avatars currently only shown in Android...
}

View file

@ -1,11 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
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';
@ -17,19 +16,18 @@ final lockingSignalEncryption = Mutex();
Future<Uint8List?> signalEncryptMessage(
int target, Uint8List plaintextContent) async {
return await lockingSignalEncryption.protect<Uint8List?>(() async {
return lockingSignalEncryption.protect<Uint8List?>(() async {
try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
final signalStore = (await getSignalStore())!;
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
SessionCipher session = SessionCipher.fromStore(signalStore, address);
final session = SessionCipher.fromStore(signalStore, address);
SignalContactPreKey? preKey = await getPreKeyByContactId(target);
SignalContactSignedPreKey? signedPreKey =
await getSignedPreKeyByContactId(target);
final preKey = await getPreKeyByContactId(target);
final signedPreKey = await getSignedPreKeyByContactId(target);
if (signedPreKey != null) {
SessionBuilder sessionBuilder = SessionBuilder.fromSignalStore(
final sessionBuilder = SessionBuilder.fromSignalStore(
signalStore,
address,
);
@ -45,20 +43,20 @@ Future<Uint8List?> signalEncryptMessage(
);
}
ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint(
final ECPublicKey? tempSignedPreKeyPublic = Curve.decodePoint(
DjbECPublicKey(Uint8List.fromList(signedPreKey.signedPreKey))
.serialize(),
1,
);
Uint8List? tempSignedPreKeySignature = Uint8List.fromList(
final Uint8List? tempSignedPreKeySignature = Uint8List.fromList(
signedPreKey.signedPreKeySignature,
);
final IdentityKey? tempIdentityKey =
await signalStore.getIdentity(address);
if (tempIdentityKey != null) {
PreKeyBundle preKeyBundle = PreKeyBundle(
final preKeyBundle = PreKeyBundle(
target,
defaultDeviceId,
preKey?.preKeyId,
@ -72,16 +70,16 @@ Future<Uint8List?> signalEncryptMessage(
try {
await sessionBuilder.processPreKeyBundle(preKeyBundle);
} catch (e) {
Log.error("could not process pre key bundle: $e");
Log.error('could not process pre key bundle: $e');
}
} else {
Log.error("did not get the identity of the remote address");
Log.error('did not get the identity of the remote address');
}
}
final ciphertext = await session.encrypt(plaintextContent);
var b = BytesBuilder();
final b = BytesBuilder();
b.add(ciphertext.serialize());
b.add(intToBytes(ciphertext.getType()));
@ -95,36 +93,34 @@ Future<Uint8List?> signalEncryptMessage(
Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
final signalStore = (await getSignalStore())!;
SessionCipher session = SessionCipher.fromStore(
final session = SessionCipher.fromStore(
signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId));
List<Uint8List>? msgs = removeLastXBytes(msg, 4);
final msgs = removeLastXBytes(msg, 4);
if (msgs == null) {
Log.error("Message requires at least 4 bytes.");
Log.error('Message requires at least 4 bytes.');
return null;
}
Uint8List body = msgs[0];
int type = bytesToInt(msgs[1]);
final body = msgs[0];
final type = bytesToInt(msgs[1]);
Uint8List plaintext;
if (type == CiphertextMessage.prekeyType) {
PreKeySignalMessage pre = PreKeySignalMessage(body);
final pre = PreKeySignalMessage(body);
plaintext = await session.decrypt(pre);
} else if (type == CiphertextMessage.whisperType) {
SignalMessage signalMsg = SignalMessage.fromSerialized(body);
final signalMsg = SignalMessage.fromSerialized(body);
plaintext = await session.decryptFromSignal(signalMsg);
} else {
Log.error("Type not known: $type");
Log.error('Type not known: $type');
return null;
}
return MessageJson.fromJson(
jsonDecode(
utf8.decode(
gzip.decode(plaintext),
),
return MessageJson.fromJson(jsonDecode(
utf8.decode(
gzip.decode(plaintext),
),
);
) as Map<String, dynamic>);
} catch (e) {
Log.error(e.toString());
return null;

View file

@ -1,12 +1,11 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/json/signal_identity.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/model/json/signal_identity.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
@ -20,40 +19,41 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
// This function runs after the clients authenticated with the server.
// It then checks if it should update a new session key
Future signalHandleNewServerConnection() async {
final UserData? user = await getUser();
Future<void> signalHandleNewServerConnection() async {
final user = await getUser();
if (user == null) return;
if (user.signalLastSignedPreKeyUpdated != null) {
DateTime fortyEightHoursAgo = DateTime.now().subtract(Duration(hours: 48));
bool isYoungerThan48Hours =
final fortyEightHoursAgo =
DateTime.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours =
(user.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated.
return;
}
}
SignedPreKeyRecord? signedPreKey = await _getNewSignalSignedPreKey();
final signedPreKey = await _getNewSignalSignedPreKey();
if (signedPreKey == null) {
Log.error("could not generate a new signed pre key!");
Log.error('could not generate a new signed pre key!');
return;
}
await updateUserdata((user) {
user.signalLastSignedPreKeyUpdated = DateTime.now();
return user;
});
Result res = await apiService.updateSignedPreKey(
final res = await apiService.updateSignedPreKey(
signedPreKey.id,
signedPreKey.getKeyPair().publicKey.serialize(),
signedPreKey.signature,
);
if (res.isError) {
Log.error("could not update the signed pre key: ${res.error}");
Log.error('could not update the signed pre key: ${res.error}');
await updateUserdata((user) {
user.signalLastSignedPreKeyUpdated = null;
return user;
});
} else {
Log.info("updated signed pre key");
Log.info('updated signed pre key');
}
}
@ -61,7 +61,7 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
final user = await getUser();
if (user == null) return [];
int start = user.currentPreKeyIndexStart;
final start = user.currentPreKeyIndexStart;
await updateUserdata((user) {
user.currentPreKeyIndexStart += 200;
return user;
@ -77,7 +77,7 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
Future<SignalIdentity?> getSignalIdentity() async {
try {
final storage = FlutterSecureStorage();
const storage = FlutterSecureStorage();
var signalIdentityJson =
await storage.read(key: SecureStorageKeys.signalIdentity);
if (signalIdentityJson == null) {
@ -85,15 +85,15 @@ Future<SignalIdentity?> getSignalIdentity() async {
}
final decoded = jsonDecode(signalIdentityJson);
signalIdentityJson = null;
return SignalIdentity.fromJson(decoded);
return SignalIdentity.fromJson(decoded as Map<String, dynamic>);
} catch (e) {
Log.error("could not load signal identity: $e");
Log.error('could not load signal identity: $e');
return null;
}
}
Future createIfNotExistsSignalIdentity() async {
final storage = FlutterSecureStorage();
Future<void> createIfNotExistsSignalIdentity() async {
const storage = FlutterSecureStorage();
final signalIdentity = await storage.read(
key: SecureStorageKeys.signalIdentity,
@ -106,7 +106,7 @@ Future createIfNotExistsSignalIdentity() async {
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true);
ConnectSignalProtocolStore signalStore =
final signalStore =
ConnectSignalProtocolStore(identityKeyPair, registrationId);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
@ -133,13 +133,13 @@ Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
return null;
}
int signedPreKeyId = user.currentSignedPreKeyIndexStart;
final signedPreKeyId = user.currentSignedPreKeyIndexStart;
await updateUserdata((user) {
user.currentSignedPreKeyIndexStart += 1;
return user;
});
final SignedPreKeyRecord signedPreKey = generateSignedPreKey(
final signedPreKey = generateSignedPreKey(
identityKeyPair,
signedPreKeyId,
);

View file

@ -23,7 +23,7 @@ Mutex requestNewKeys = Mutex();
DateTime lastPreKeyRequest = DateTime.now().subtract(Duration(hours: 1));
DateTime lastSignedPreKeyRequest = DateTime.now().subtract(Duration(hours: 1));
Future requestNewPrekeysForContact(int contactId) async {
Future<void> requestNewPrekeysForContact(int contactId) async {
if (lastPreKeyRequest
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
return;
@ -59,7 +59,7 @@ Future<SignalContactPreKey?> getPreKeyByContactId(int contactId) async {
return twonlyDB.signalDao.popPreKeyByContactId(contactId);
}
Future requestNewSignedPreKeyForContact(int contactId) async {
Future<void> requestNewSignedPreKeyForContact(int contactId) async {
if (lastSignedPreKeyRequest
.isAfter(DateTime.now().subtract(Duration(seconds: 60)))) {
Log.info("last signed pre request was 60s before");

View file

@ -77,7 +77,7 @@ Future<bool> createNewSignalSession(Response_UserData userData) async {
}
}
Future deleteSessionWithTarget(int target) async {
Future<void> deleteSessionWithTarget(int target) async {
ConnectSignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) return;
final address = SignalProtocolAddress(target.toString(), defaultDeviceId);

View file

@ -1,23 +1,24 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:path_provider/path_provider.dart';
Future<void> createThumbnails(String directoryPath) async {
final directory = Directory(directoryPath);
final outputDirectory = await getTemporaryDirectory();
if (await directory.exists()) {
final List<FileSystemEntity> files = directory.listSync();
if (directory.existsSync()) {
final files = directory.listSync();
for (var file in files) {
for (final file in files) {
if (file is File) {
final String filePath = file.path;
final String fileExtension = filePath.split('.').last.toLowerCase();
final filePath = file.path;
final fileExtension = filePath.split('.').last.toLowerCase();
if (['jpg', 'jpeg', 'png'].contains(fileExtension)) {
// Create thumbnail for images
@ -26,23 +27,18 @@ Future<void> createThumbnails(String directoryPath) async {
final thumbnailFile =
File('${outputDirectory.path}/${file.uri.pathSegments.last}');
await thumbnailFile.writeAsBytes(thumbnail!.buffer.asUint8List());
print('Thumbnail created for image: ${file.uri.pathSegments.last}');
} else if (['mp4', 'mov', 'avi'].contains(fileExtension)) {
// Create thumbnail for videos
print('Thumbnail created for video: ${file.uri.pathSegments.last}');
}
}
}
} else {
print('Directory does not exist: $directoryPath');
}
}
Future createThumbnailsForImage(File file) async {
final String fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != "png") {
Log.error("Could not create thumbnail for image. $fileExtension != .png");
Future<void> createThumbnailsForImage(File file) async {
final fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != 'png') {
Log.error('Could not create thumbnail for image. $fileExtension != .png');
return;
}
@ -56,45 +52,44 @@ Future createThumbnailsForImage(File file) async {
);
if (imageBytesCompressed == null) {
Log.error("Could not compress the image");
Log.error('Could not compress the image');
return;
}
File thumbnailFile = getThumbnailPath(file);
final thumbnailFile = getThumbnailPath(file);
await thumbnailFile.writeAsBytes(imageBytesCompressed);
} catch (e) {
Log.error("Could not compress the image got :$e");
Log.error('Could not compress the image got :$e');
}
}
Future createThumbnailsForVideo(File file) async {
final String fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != "mp4") {
Log.error("Could not create thumbnail for video. $fileExtension != .mp4");
Future<void> createThumbnailsForVideo(File file) async {
final fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != 'mp4') {
Log.error('Could not create thumbnail for video. $fileExtension != .mp4');
return;
}
try {
await VideoThumbnail.thumbnailFile(
video: file.path,
imageFormat: ImageFormat.PNG,
thumbnailPath: getThumbnailPath(file).path,
maxWidth: 450,
quality: 75,
);
} catch (e) {
Log.error("Could not create the video thumbnail: $e");
Log.error('Could not create the video thumbnail: $e');
}
}
File getThumbnailPath(File file) {
String originalFileName = file.uri.pathSegments.last;
String fileNameWithoutExtension = originalFileName.split('.').first;
String fileExtension = originalFileName.split('.').last;
if (fileExtension == "mp4") {
fileExtension = "png";
final originalFileName = file.uri.pathSegments.last;
final fileNameWithoutExtension = originalFileName.split('.').first;
var fileExtension = originalFileName.split('.').last;
if (fileExtension == 'mp4') {
fileExtension = 'png';
}
String newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
final newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
Directory(file.parent.path).createSync();
return File(join(file.parent.path, newFileName));
}

View file

@ -8,7 +8,7 @@ import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
Future enableTwonlySafe(String password) async {
Future<void> enableTwonlySafe(String password) async {
final user = await getUser();
if (user == null) return;
@ -24,7 +24,7 @@ Future enableTwonlySafe(String password) async {
performTwonlySafeBackup(force: true);
}
Future disableTwonlySafe() async {
Future<void> disableTwonlySafe() async {
final serverUrl = await getTwonlySafeBackupUrl();
if (serverUrl != null) {
try {

View file

@ -18,7 +18,7 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
Future performTwonlySafeBackup({bool force = false}) async {
Future<void> performTwonlySafeBackup({bool force = false}) async {
final user = await getUser();
if (user == null || user.twonlySafeBackup == null || user.isDemoUser) {
@ -27,38 +27,39 @@ Future performTwonlySafeBackup({bool force = false}) async {
if (user.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) {
Log.warn("Backup upload is already pending.");
Log.warn('Backup upload is already pending.');
return;
}
DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
final DateTime? lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) {
if (lastUpdateTime.isAfter(DateTime.now().subtract(Duration(days: 1)))) {
if (lastUpdateTime
.isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
return;
}
}
Log.info("Starting new twonly Safe-Backup!");
Log.info('Starting new twonly Safe-Backup!');
final baseDir = (await getApplicationSupportDirectory()).path;
final backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
await backupDir.create(recursive: true);
final backupDatabaseFile =
File(join(backupDir.path, "twonly_database.backup.sqlite"));
File(join(backupDir.path, 'twonly_database.backup.sqlite'));
final backupDatabaseFileCleaned =
File(join(backupDir.path, "twonly_database.backup.cleaned.sqlite"));
File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite'));
// copy database
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
final originalDatabase = File(join(baseDir, 'twonly_database.sqlite'));
await originalDatabase.copy(backupDatabaseFile.path);
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final backupDB = TwonlyDatabase(
driftDatabase(
name: "twonly_database.backup",
name: 'twonly_database.backup',
native: DriftNativeOptions(
databaseDirectory: () async {
return backupDir;
@ -74,24 +75,26 @@ Future performTwonlySafeBackup({bool force = false}) async {
await backupDB.printTableSizes();
backupDB.close();
await backupDB.close();
var secureStorageBackup = {};
final storage = FlutterSecureStorage();
// ignore: inference_failure_on_collection_literal
final secureStorageBackup = {};
const storage = FlutterSecureStorage();
secureStorageBackup[SecureStorageKeys.signalIdentity] =
await storage.read(key: SecureStorageKeys.signalIdentity);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
var userBackup = await getUser();
final userBackup = await getUser();
if (userBackup == null) return;
// FILTER settings which should not be in the backup
userBackup.twonlySafeBackup = null;
userBackup.lastImageSend = null;
userBackup.todaysImageCounter = null;
userBackup.lastPlanBallance = "";
userBackup.additionalUserInvites = "";
userBackup.signalLastSignedPreKeyUpdated = null;
userBackup
..twonlySafeBackup = null
..lastImageSend = null
..todaysImageCounter = null
..lastPlanBallance = ''
..additionalUserInvites = ''
..signalLastSignedPreKeyUpdated = null;
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
@ -101,8 +104,8 @@ Future performTwonlySafeBackup({bool force = false}) async {
await backupDatabaseFile.delete();
await backupDatabaseFileCleaned.delete();
Log.info("twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}");
Log.info("secureStorageLength = ${jsonEncode(secureStorageBackup).length}");
Log.info('twonlyDatabaseLength = ${twonlyDatabaseBytes.lengthInBytes}');
Log.info('secureStorageLength = ${jsonEncode(secureStorageBackup).length}');
final backupProto = TwonlySafeBackupContent(
secureStorageJson: jsonEncode(secureStorageBackup),
@ -115,7 +118,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
if (user.twonlySafeBackup!.lastBackupDone == null ||
user.twonlySafeBackup!.lastBackupDone!
.isAfter(DateTime.now().subtract(Duration(days: 90)))) {
.isAfter(DateTime.now().subtract(const Duration(days: 90)))) {
force = true;
}
@ -124,7 +127,7 @@ Future performTwonlySafeBackup({bool force = false}) async {
if (lastHash != null && !force) {
if (backupHash == lastHash) {
Log.info("Since last backup nothing has changed.");
Log.info('Since last backup nothing has changed.');
return;
}
}
@ -144,25 +147,25 @@ Future performTwonlySafeBackup({bool force = false}) async {
nonce: nonce,
);
final encryptedBackupBytes = (TwonlySafeBackupEncrypted(
final encryptedBackupBytes = TwonlySafeBackupEncrypted(
mac: secretBox.mac.bytes,
nonce: nonce,
cipherText: secretBox.cipherText,
)).writeToBuffer();
).writeToBuffer();
Log.info("Backup files created.");
Log.info('Backup files created.');
var encryptedBackupBytesFile =
File(join(backupDir.path, "twonly_safe.backup"));
final encryptedBackupBytesFile =
File(join(backupDir.path, 'twonly_safe.backup'));
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
Log.info(
"Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.");
'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.');
if (user.backupServer != null) {
if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
Log.error("Backup is to big for the alternative backup server.");
Log.error('Backup is to big for the alternative backup server.');
await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
return user;
@ -172,20 +175,20 @@ Future performTwonlySafeBackup({bool force = false}) async {
}
final task = UploadTask.fromFile(
taskId: "backup",
taskId: 'backup',
file: encryptedBackupBytesFile,
httpRequestMethod: "PUT",
httpRequestMethod: 'PUT',
url: (await getTwonlySafeBackupUrl())!,
// requiresWiFi: true,
priority: 5,
post: 'binary',
retries: 2,
headers: {
"Content-Type": "application/octet-stream",
'Content-Type': 'application/octet-stream',
},
);
if (await FileDownloader().enqueue(task)) {
Log.info("Starting upload from twonly Safe backup.");
Log.info('Starting upload from twonly Safe backup.');
await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
user.twonlySafeBackup!.lastBackupDone = DateTime.now();
@ -194,15 +197,15 @@ Future performTwonlySafeBackup({bool force = false}) async {
});
gUpdateBackupView();
} else {
Log.error("Error starting UploadTask for twonly Safe.");
Log.error('Error starting UploadTask for twonly Safe.');
}
}
Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
Log.error(
"twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}");
'twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}');
await updateUserdata((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
@ -211,7 +214,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
});
} else if (update.status == TaskStatus.complete) {
Log.error(
"twonly Safe uploaded with status code ${update.responseStatusCode}");
'twonly Safe uploaded with status code ${update.responseStatusCode}');
await updateUserdata((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState =
@ -220,7 +223,7 @@ Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
return user;
});
} else {
Log.info("Backup is in state: ${update.status}");
Log.info('Backup is in state: ${update.status}');
return;
}
gUpdateBackupView();

View file

@ -1,3 +1,5 @@
// ignore_for_file: avoid_dynamic_calls
import 'dart:convert';
import 'dart:io';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
@ -15,19 +17,19 @@ import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
Future recoverTwonlySafe(
Future<void> recoverTwonlySafe(
String username,
String password,
BackupServer? server,
) async {
final (backupId, encryptionKey) = await getMasterKey(password, username);
String? backupServerUrl =
final backupServerUrl =
await getTwonlySafeBackupUrlFromServer(backupId, server);
if (backupServerUrl == null) {
Log.error("Could not create backup url");
throw Exception("Could not create backup server url");
Log.error('Could not create backup url');
throw Exception('Could not create backup server url');
}
late Uint8List backupData;
@ -39,7 +41,7 @@ Future recoverTwonlySafe(
});
} catch (e) {
Log.error('Error fetching backup: $e');
throw Exception("Backup server could not be reached. ($e)");
throw Exception('Backup server could not be reached. ($e)');
}
switch (response.statusCode) {
@ -55,19 +57,18 @@ Future recoverTwonlySafe(
throw Exception('Unexpected error: ${response.statusCode}');
}
return await handleBackupData(encryptionKey, backupData);
return handleBackupData(encryptionKey, backupData);
}
Future handleBackupData(
Future<void> handleBackupData(
Uint8List encryptionKey,
Uint8List backupData,
) async {
TwonlySafeBackupEncrypted encryptedBackup =
TwonlySafeBackupEncrypted.fromBuffer(
final encryptedBackup = TwonlySafeBackupEncrypted.fromBuffer(
backupData,
);
SecretBox secretBox = SecretBox(
final secretBox = SecretBox(
encryptedBackup.cipherText,
nonce: encryptedBackup.nonce,
mac: Mac(encryptedBackup.mac),
@ -80,12 +81,12 @@ Future handleBackupData(
final plaintextBytes = gzip.decode(compressedBytes);
TwonlySafeBackupContent backupContent = TwonlySafeBackupContent.fromBuffer(
final backupContent = TwonlySafeBackupContent.fromBuffer(
plaintextBytes,
);
final baseDir = (await getApplicationSupportDirectory()).path;
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
final originalDatabase = File(join(baseDir, 'twonly_database.sqlite'));
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
/// When restoring the last message ID must be increased otherwise
@ -106,33 +107,33 @@ Future handleBackupData(
if (randomUserId != null) {
// for each day add 400 message ids
var dummyMessagesCounter = (lastMessageSend + 1) * 400;
final dummyMessagesCounter = (lastMessageSend + 1) * 400;
Log.info(
"Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.");
'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.');
for (var i = 0; i < dummyMessagesCounter; i++) {
await database.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(randomUserId),
kind: Value(MessageKind.ack),
acknowledgeByServer: Value(true),
errorWhileSending: Value(true),
kind: const Value(MessageKind.ack),
acknowledgeByServer: const Value(true),
errorWhileSending: const Value(true),
),
);
}
await database.messagesDao.deleteAllMessagesByContactId(randomUserId);
}
final storage = FlutterSecureStorage();
const storage = FlutterSecureStorage();
final secureStorage = jsonDecode(backupContent.secureStorageJson);
await storage.write(
key: SecureStorageKeys.signalIdentity,
value: secureStorage[SecureStorageKeys.signalIdentity]);
value: secureStorage[SecureStorageKeys.signalIdentity] as String);
await storage.write(
key: SecureStorageKeys.signalSignedPreKey,
value: secureStorage[SecureStorageKeys.signalSignedPreKey]);
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String);
await storage.write(
key: SecureStorageKeys.userData,
value: secureStorage[SecureStorageKeys.userData]);
value: secureStorage[SecureStorageKeys.userData] as String);
}

View file

@ -15,9 +15,9 @@ class KeyValueStore {
final file = File(filePath);
// Check if the file exists
if (await file.exists()) {
if (file.existsSync()) {
final contents = await file.readAsString();
return jsonDecode(contents);
return jsonDecode(contents) as Map<String, dynamic>;
} else {
return null; // File does not exist
}

View file

@ -43,13 +43,6 @@ Future<String> loadLogFile() async {
}
}
Future cleanLogFile() async {
final str = await loadLogFile();
if (str.contains("secureStorageBytes")) {
deleteLogFile();
}
}
Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log');

View file

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:math';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -12,9 +13,9 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/log.dart';
@ -51,10 +52,10 @@ Future<String?> saveVideoToGallery(String videoPath) async {
}
Uint8List getRandomUint8List(int length) {
final Random random = Random.secure();
final Uint8List randomBytes = Uint8List(length);
final random = Random.secure();
final randomBytes = Uint8List(length);
for (int i = 0; i < length; i++) {
for (var i = 0; i < length; i++) {
randomBytes[i] = random.nextInt(256); // Generate a random byte (0-255)
}
@ -62,6 +63,7 @@ Uint8List getRandomUint8List(int length) {
}
String errorCodeToText(BuildContext context, ErrorCode code) {
// ignore: exhaustive_cases
switch (code) {
case ErrorCode.InternalError:
return context.lang.errorInternalError;
@ -81,22 +83,21 @@ String errorCodeToText(BuildContext context, ErrorCode code) {
return context.lang.errorVoucherInvalid;
case ErrorCode.PlanUpgradeNotYearly:
return context.lang.errorPlanUpgradeNotYearly;
default:
return code.toString(); // Fallback for unrecognized keys
}
return code.toString(); // Fallback for unrecognized keys
}
String formatDuration(int seconds) {
if (seconds < 60) {
return '$seconds Sec.';
} else if (seconds < 3600) {
int minutes = seconds ~/ 60;
final minutes = seconds ~/ 60;
return '$minutes Min.';
} else if (seconds < 86400) {
int hours = seconds ~/ 3600;
final hours = seconds ~/ 3600;
return '$hours Hrs.'; // Assuming "Stu." is for hours
} else {
int days = seconds ~/ 86400;
final days = seconds ~/ 86400;
return '$days Days';
}
}
@ -107,20 +108,19 @@ InputDecoration getInputDecoration(BuildContext context, String hintText) {
return InputDecoration(
hintText: hintText,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(9.0),
borderSide: BorderSide(color: primaryColor, width: 1.0),
borderRadius: BorderRadius.circular(9),
borderSide: BorderSide(color: primaryColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.outline, width: 1.0),
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
),
contentPadding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
);
}
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
var result = await FlutterImageCompress.compressWithList(
final result = await FlutterImageCompress.compressWithList(
imageBytes,
quality: 90,
);
@ -130,8 +130,8 @@ Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
Future<bool> authenticateUser(String localizedReason,
{bool force = true}) async {
try {
final LocalAuthentication auth = LocalAuthentication();
bool didAuthenticate = await auth.authenticate(
final auth = LocalAuthentication();
final didAuthenticate = await auth.authenticate(
localizedReason: localizedReason,
options: const AuthenticationOptions(useErrorDialogs: false));
if (didAuthenticate) {
@ -147,31 +147,30 @@ Future<bool> authenticateUser(String localizedReason,
}
Uint8List intToBytes(int value) {
final byteData = ByteData(4);
byteData.setInt32(0, value, Endian.big);
final byteData = ByteData(4)..setInt32(0, value);
return byteData.buffer.asUint8List();
}
int bytesToInt(Uint8List bytes) {
final byteData = ByteData.sublistView(bytes);
return byteData.getInt32(0, Endian.big);
return byteData.getInt32(0);
}
List<Uint8List>? removeLastXBytes(Uint8List original, int count) {
if (original.length < count) {
return null;
}
final newList = Uint8List(original.length - count);
newList.setAll(0, original.sublist(0, original.length - count));
final newList = Uint8List(original.length - count)
..setAll(0, original.sublist(0, original.length - count));
final lastXBytes = original.sublist(original.length - count);
return [newList, lastXBytes];
}
bool isDarkMode(BuildContext context) {
ThemeMode? selectedTheme = context.read<SettingsChangeProvider>().themeMode;
final selectedTheme = context.read<SettingsChangeProvider>().themeMode;
bool isDarkMode =
final isDarkMode =
MediaQuery.of(context).platformBrightness == Brightness.dark;
return selectedTheme == ThemeMode.dark ||
@ -188,20 +187,20 @@ bool isToday(DateTime lastImageSend) {
InputDecoration inputTextMessageDeco(BuildContext context) {
return InputDecoration(
hintText: context.lang.chatListDetailInput,
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderRadius: BorderRadius.circular(20),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide: BorderSide(color: Colors.grey, width: 2.0),
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(color: Colors.grey, width: 2),
),
);
}
@ -213,8 +212,8 @@ String truncateString(String input, {int maxLength = 20}) {
return input;
}
Future insertDemoContacts() async {
List<String> commonUsernames = [
Future<void> insertDemoContacts() async {
final commonUsernames = <String>[
'James',
'Mary',
'John',
@ -236,7 +235,7 @@ Future insertDemoContacts() async {
'Thomas',
'Karen',
];
final List<Map<String, dynamic>> contactConfigs = [
final contactConfigs = <Map<String, dynamic>>[
{'count': 3, 'requested': true},
{'count': 4, 'requested': false, 'accepted': true},
{'count': 1, 'accepted': true, 'blocked': true},
@ -245,43 +244,44 @@ Future insertDemoContacts() async {
{'count': 1, 'requested': false},
];
int counter = 0;
var counter = 0;
for (var config in contactConfigs) {
for (int i = 0; i < config['count']; i++) {
for (final config in contactConfigs) {
for (var i = 0; i < (config['count'] as int); i++) {
if (counter >= commonUsernames.length) {
break;
}
String username = commonUsernames[counter];
int userId = Random().nextInt(1000000);
final username = commonUsernames[counter];
final userId = Random().nextInt(1000000);
await twonlyDB.contactsDao.insertContact(
ContactsCompanion(
username: Value(username),
userId: Value(userId),
requested: Value(config['requested'] ?? false),
accepted: Value(config['accepted'] ?? false),
blocked: Value(config['blocked'] ?? false),
archived: Value(config['archived'] ?? false),
pinned: Value(config['pinned'] ?? false),
requested: Value(config['requested'] as bool? ?? false),
accepted: Value(config['accepted'] as bool? ?? false),
blocked: Value(config['blocked'] as bool? ?? false),
archived: Value(config['archived'] as bool? ?? false),
pinned: Value(config['pinned'] as bool? ?? false),
),
);
if (config['accepted'] ?? false) {
if (config['accepted'] as bool? ?? false) {
for (var i = 0; i < 20; i++) {
int chatId = Random().nextInt(chatMessages.length);
final chatId = Random().nextInt(chatMessages.length);
await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(userId),
kind: Value(MessageKind.textMessage),
sendAt: Value(chatMessages[chatId][1]),
acknowledgeByServer: Value(true),
acknowledgeByUser: Value(true),
kind: const Value(MessageKind.textMessage),
sendAt: Value(chatMessages[chatId][1] as DateTime),
acknowledgeByServer: const Value(true),
acknowledgeByUser: const Value(true),
messageOtherId:
Value(Random().nextBool() ? Random().nextInt(10000) : null),
// responseToOtherMessageId: Value(content.responseToMessageId),
// responseToMessageId: Value(content.responseToOtherMessageId),
downloadState: Value(DownloadState.downloaded),
downloadState: const Value(DownloadState.downloaded),
contentJson: Value(
jsonEncode(TextMessageContent(text: chatMessages[chatId][0])),
jsonEncode(TextMessageContent(
text: chatMessages[chatId][0] as String)),
),
),
);
@ -292,91 +292,94 @@ Future insertDemoContacts() async {
}
}
Future createFakeDemoData() async {
Future<void> createFakeDemoData() async {
await insertDemoContacts();
}
List<List<dynamic>> chatMessages = [
[
"Lorem ipsum dolor sit amet.",
DateTime.now().subtract(Duration(minutes: 20))
'Lorem ipsum dolor sit amet.',
DateTime.now().subtract(const Duration(minutes: 20))
],
[