mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-16 06:32:54 +00:00
fix: media file appears as a white square and is not listed.
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
cdaca940df
commit
cc0b88718b
13 changed files with 285 additions and 192 deletions
13
.github/workflows/dev_github.yml
vendored
13
.github/workflows/dev_github.yml
vendored
|
|
@ -25,8 +25,11 @@ jobs:
|
|||
- name: Cloning sub-repos
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: Check flutter code
|
||||
run: |
|
||||
flutter pub get
|
||||
flutter analyze
|
||||
flutter test
|
||||
- name: flutter pub get
|
||||
run: flutter pub get
|
||||
|
||||
- name: flutter analyze
|
||||
run: flutter analyze
|
||||
|
||||
- name: flutter test
|
||||
run: flutter test
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Changelog
|
||||
|
||||
## 0.0.98
|
||||
## 0.0.99
|
||||
|
||||
- New: Groups can now collect flames as well
|
||||
- New: Background execution to pre-load messages
|
||||
|
|
@ -8,10 +8,12 @@
|
|||
- Improve: Video compression with progress updates
|
||||
- Improve: Show message "Flames restored"
|
||||
- Improve: Show toast message if user was added via QR
|
||||
- Fix: Media file appears as a white square and is not listed.
|
||||
- Fix: Issue with media files required to be reuploaded
|
||||
- Fix: Problem during contact requests
|
||||
- Fix: Problem with deleting a contact
|
||||
- Fix: Problem with restoring from backup
|
||||
- Fix: Issue with the log file
|
||||
|
||||
## 0.0.96
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ import 'package:twonly/src/utils/storage.dart';
|
|||
void main() async {
|
||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
|
||||
globalApplicationSupportDirectory =
|
||||
(await getApplicationSupportDirectory()).path;
|
||||
|
||||
await initFCMService();
|
||||
|
||||
final user = await getUser();
|
||||
|
|
@ -53,10 +57,6 @@ void main() async {
|
|||
await deleteLocalUserData();
|
||||
}
|
||||
|
||||
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
|
||||
globalApplicationSupportDirectory =
|
||||
(await getApplicationSupportDirectory()).path;
|
||||
|
||||
initLogger();
|
||||
|
||||
final settingsController = SettingsChangeProvider();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/tables/receipts.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -9,7 +10,9 @@ import 'package:twonly/src/utils/log.dart';
|
|||
|
||||
part 'receipts.dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [Receipts, Messages, MessageActions, ReceivedReceipts])
|
||||
@DriftAccessor(
|
||||
tables: [Receipts, Messages, MessageActions, ReceivedReceipts, Contacts],
|
||||
)
|
||||
class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
|
|
@ -60,6 +63,17 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
)),
|
||||
))
|
||||
.go();
|
||||
|
||||
final deletedContacts = await (select(
|
||||
contacts,
|
||||
)..where((t) => t.accountDeleted.equals(true))).get();
|
||||
|
||||
for (final contact in deletedContacts) {
|
||||
await (delete(receipts)..where(
|
||||
(t) => t.contactId.equals(contact.userId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Receipt?> insertReceipt(ReceiptsCompanion entry) async {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import 'package:web_socket_channel/io.dart';
|
|||
|
||||
final lockConnecting = Mutex();
|
||||
final lockRetransStore = Mutex();
|
||||
final lockAuthentication = Mutex();
|
||||
|
||||
/// The ApiProvider is responsible for communicating with the server.
|
||||
/// It handles errors and does automatically tries to reconnect on
|
||||
|
|
@ -86,7 +87,6 @@ class ApiService {
|
|||
|
||||
// Function is called after the user is authenticated at the server
|
||||
Future<void> onAuthenticated() async {
|
||||
isAuthenticated = true;
|
||||
await initFCMAfterAuthenticated();
|
||||
globalCallbackConnectionState(isConnected: true);
|
||||
|
||||
|
|
@ -157,8 +157,9 @@ class ApiService {
|
|||
if (connectivitySubscription != null) {
|
||||
return;
|
||||
}
|
||||
connectivitySubscription =
|
||||
Connectivity().onConnectivityChanged.listen((result) async {
|
||||
connectivitySubscription = Connectivity().onConnectivityChanged.listen((
|
||||
result,
|
||||
) async {
|
||||
if (!result.contains(ConnectivityResult.none)) {
|
||||
await connect();
|
||||
}
|
||||
|
|
@ -355,7 +356,6 @@ class ApiService {
|
|||
return Result.error(ErrorCode.InternalError);
|
||||
}
|
||||
if (res.error == ErrorCode.SessionNotAuthenticated) {
|
||||
isAuthenticated = false;
|
||||
if (authenticated) {
|
||||
await authenticate();
|
||||
if (isAuthenticated) {
|
||||
|
|
@ -387,8 +387,9 @@ class ApiService {
|
|||
|
||||
Future<bool> tryAuthenticateWithToken(int userId) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final apiAuthToken =
|
||||
await storage.read(key: SecureStorageKeys.apiAuthToken);
|
||||
final apiAuthToken = await storage.read(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
);
|
||||
final user = await getUser();
|
||||
|
||||
if (apiAuthToken != null && user != null) {
|
||||
|
|
@ -412,6 +413,7 @@ class ApiService {
|
|||
|
||||
if (result.isSuccess) {
|
||||
Log.info('websocket is authenticated');
|
||||
isAuthenticated = true;
|
||||
if (globalIsInBackgroundTask) {
|
||||
await onAuthenticated();
|
||||
} else {
|
||||
|
|
@ -433,60 +435,66 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<void> authenticate() async {
|
||||
if (isAuthenticated) return;
|
||||
if (await getSignalIdentity() == null) {
|
||||
return;
|
||||
}
|
||||
return lockAuthentication.protect(() async {
|
||||
if (isAuthenticated) return;
|
||||
if (await getSignalIdentity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final userData = await getUser();
|
||||
if (userData == null) return;
|
||||
final userData = await getUser();
|
||||
if (userData == null) return;
|
||||
|
||||
if (await tryAuthenticateWithToken(userData.userId)) {
|
||||
return;
|
||||
}
|
||||
if (await tryAuthenticateWithToken(userData.userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final handshake = Handshake()
|
||||
..getAuthChallenge = Handshake_GetAuthChallenge();
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
final handshake = Handshake()
|
||||
..getAuthChallenge = Handshake_GetAuthChallenge();
|
||||
final req = createClientToServerFromHandshake(handshake);
|
||||
|
||||
final result = await sendRequestSync(req, authenticated: false);
|
||||
if (result.isError) {
|
||||
Log.warn('could not request auth challenge', result);
|
||||
return;
|
||||
}
|
||||
final result = await sendRequestSync(req, authenticated: false);
|
||||
if (result.isError) {
|
||||
Log.warn('could not request auth challenge', result);
|
||||
return;
|
||||
}
|
||||
|
||||
final challenge = result.value.authchallenge;
|
||||
final challenge = result.value.authchallenge;
|
||||
|
||||
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
|
||||
if (privKey == null) return;
|
||||
final random = getRandomUint8List(32);
|
||||
final signature = sign(privKey.serialize(), challenge as Uint8List, random);
|
||||
privKey = null;
|
||||
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
|
||||
if (privKey == null) return;
|
||||
final random = getRandomUint8List(32);
|
||||
final signature = sign(
|
||||
privKey.serialize(),
|
||||
challenge as Uint8List,
|
||||
random,
|
||||
);
|
||||
privKey = null;
|
||||
|
||||
final getAuthToken = Handshake_GetAuthToken()
|
||||
..response = signature
|
||||
..userId = Int64(userData.userId);
|
||||
final getAuthToken = Handshake_GetAuthToken()
|
||||
..response = signature
|
||||
..userId = Int64(userData.userId);
|
||||
|
||||
final getauthtoken = Handshake()..getAuthToken = getAuthToken;
|
||||
final getauthtoken = Handshake()..getAuthToken = getAuthToken;
|
||||
|
||||
final 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}');
|
||||
return;
|
||||
}
|
||||
final result2 = await sendRequestSync(req2, authenticated: false);
|
||||
if (result2.isError) {
|
||||
Log.error('could not send auth response: ${result2.error}');
|
||||
return;
|
||||
}
|
||||
|
||||
final apiAuthToken = result2.value.authtoken as Uint8List;
|
||||
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||
final apiAuthToken = result2.value.authtoken as Uint8List;
|
||||
final apiAuthTokenB64 = base64Encode(apiAuthToken);
|
||||
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
value: apiAuthTokenB64,
|
||||
);
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
value: apiAuthTokenB64,
|
||||
);
|
||||
|
||||
await tryAuthenticateWithToken(userData.userId);
|
||||
await tryAuthenticateWithToken(userData.userId);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Result> register(
|
||||
|
|
@ -505,8 +513,9 @@ class ApiService {
|
|||
|
||||
final register = Handshake_Register()
|
||||
..username = username
|
||||
..publicIdentityKey =
|
||||
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize()
|
||||
..publicIdentityKey = (await signalStore.getIdentityKeyPair())
|
||||
.getPublicKey()
|
||||
.serialize()
|
||||
..registrationId = Int64(signalIdentity.registrationId)
|
||||
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
||||
..signedPrekeySignature = signedPreKey.signature
|
||||
|
|
@ -526,8 +535,10 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<void> checkForDeletedUsernames() async {
|
||||
final users = await twonlyDB.contactsDao
|
||||
.getContactsByUsername('[deleted]', username2: '[Unknown]');
|
||||
final users = await twonlyDB.contactsDao.getContactsByUsername(
|
||||
'[deleted]',
|
||||
username2: '[Unknown]',
|
||||
);
|
||||
for (final user in users) {
|
||||
final userData = await getUserById(user.userId);
|
||||
if (userData != null) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||
hide Message;
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
|
|
@ -31,7 +32,7 @@ Future<void> handleMedia(
|
|||
message.senderId != fromUserId ||
|
||||
message.mediaId == null) {
|
||||
Log.warn(
|
||||
'Got reupload for a message that either does not exists or sender != fromUserId or not a media file',
|
||||
'Got reupload from $fromUserId for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -53,8 +54,9 @@ Future<void> handleMedia(
|
|||
),
|
||||
);
|
||||
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
message.mediaId!,
|
||||
);
|
||||
|
||||
if (mediaFile != null) {
|
||||
unawaited(startDownloadMedia(mediaFile, false));
|
||||
|
|
@ -89,56 +91,64 @@ Future<void> handleMedia(
|
|||
}
|
||||
}
|
||||
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
MediaFilesCompanion(
|
||||
downloadState: const Value(DownloadState.pending),
|
||||
type: Value(mediaType),
|
||||
requiresAuthentication: Value(media.requiresAuthentication),
|
||||
displayLimitInMilliseconds: Value(
|
||||
displayLimitInMilliseconds,
|
||||
),
|
||||
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
|
||||
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
|
||||
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
|
||||
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
);
|
||||
late MediaFile? mediaFile;
|
||||
late Message? message;
|
||||
|
||||
if (mediaFile == null) {
|
||||
Log.error('Could not insert media file into database');
|
||||
return;
|
||||
}
|
||||
await twonlyDB.transaction(() async {
|
||||
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
MediaFilesCompanion(
|
||||
downloadState: const Value(DownloadState.pending),
|
||||
type: Value(mediaType),
|
||||
requiresAuthentication: Value(media.requiresAuthentication),
|
||||
displayLimitInMilliseconds: Value(
|
||||
displayLimitInMilliseconds,
|
||||
),
|
||||
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
|
||||
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
|
||||
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
|
||||
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
);
|
||||
|
||||
final message = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
messageId: Value(media.senderMessageId),
|
||||
senderId: Value(fromUserId),
|
||||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaFile.mediaId),
|
||||
type: Value(MessageType.media.name),
|
||||
additionalMessageData: Value.absentIfNull(
|
||||
media.hasAdditionalMessageData()
|
||||
? Uint8List.fromList(media.additionalMessageData)
|
||||
: null,
|
||||
if (mediaFile == null) {
|
||||
Log.error('Could not insert media file into database');
|
||||
return;
|
||||
}
|
||||
|
||||
message = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
messageId: Value(media.senderMessageId),
|
||||
senderId: Value(fromUserId),
|
||||
groupId: Value(groupId),
|
||||
mediaId: Value(mediaFile!.mediaId),
|
||||
type: Value(MessageType.media.name),
|
||||
additionalMessageData: Value.absentIfNull(
|
||||
media.hasAdditionalMessageData()
|
||||
? Uint8List.fromList(media.additionalMessageData)
|
||||
: null,
|
||||
),
|
||||
quotesMessageId: Value(
|
||||
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
||||
),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
quotesMessageId: Value(
|
||||
media.hasQuoteMessageId() ? media.quoteMessageId : null,
|
||||
),
|
||||
createdAt: Value(fromTimestamp(media.timestamp)),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
if (message != null) {
|
||||
await twonlyDB.groupsDao
|
||||
.increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp));
|
||||
Log.info('Inserted a new media message with ID: ${message.messageId}');
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(
|
||||
groupId,
|
||||
fromTimestamp(media.timestamp),
|
||||
);
|
||||
Log.info('Inserted a new media message with ID: ${message!.messageId}');
|
||||
await incFlameCounter(
|
||||
message.groupId,
|
||||
message!.groupId,
|
||||
true,
|
||||
fromTimestamp(media.timestamp),
|
||||
);
|
||||
|
||||
unawaited(startDownloadMedia(mediaFile, false));
|
||||
unawaited(startDownloadMedia(mediaFile!, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,8 +173,9 @@ Future<void> handleMediaUpdate(
|
|||
);
|
||||
return;
|
||||
}
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
message.mediaId!,
|
||||
);
|
||||
if (mediaFile == null) {
|
||||
Log.info(
|
||||
'Got media file update, but media file was not found ${message.mediaId}',
|
||||
|
|
@ -203,8 +214,9 @@ Future<void> handleMediaUpdate(
|
|||
reuploadRequestedBy: Value(reuploadRequestedBy),
|
||||
),
|
||||
);
|
||||
final mediaFileUpdated =
|
||||
await MediaFileService.fromMediaId(mediaFile.mediaId);
|
||||
final mediaFileUpdated = await MediaFileService.fromMediaId(
|
||||
mediaFile.mediaId,
|
||||
);
|
||||
if (mediaFileUpdated != null) {
|
||||
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
|
||||
mediaFileUpdated.uploadRequestPath.deleteSync();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import 'package:drift/drift.dart';
|
|||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -42,15 +41,16 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
Log.info('Starting new twonly Backup!');
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
final baseDir = globalApplicationSupportDirectory;
|
||||
|
||||
final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
|
||||
await backupDir.create(recursive: true);
|
||||
|
||||
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
|
||||
|
||||
final backupDatabaseFileCleaned =
|
||||
File(join(backupDir.path, 'twonly.backup.cleaned.sqlite'));
|
||||
final backupDatabaseFileCleaned = File(
|
||||
join(backupDir.path, 'twonly.backup.cleaned.sqlite'),
|
||||
);
|
||||
|
||||
// copy database
|
||||
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
|
||||
|
|
@ -70,8 +70,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
await backupDB.deleteDataForTwonlySafe();
|
||||
|
||||
await backupDB
|
||||
.customStatement('VACUUM INTO ?', [backupDatabaseFileCleaned.path]);
|
||||
await backupDB.customStatement('VACUUM INTO ?', [
|
||||
backupDatabaseFileCleaned.path,
|
||||
]);
|
||||
|
||||
await backupDB.printTableSizes();
|
||||
|
||||
|
|
@ -80,10 +81,11 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
// 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);
|
||||
secureStorageBackup[SecureStorageKeys.signalIdentity] = await storage.read(
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
);
|
||||
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = await storage
|
||||
.read(key: SecureStorageKeys.signalSignedPreKey);
|
||||
|
||||
final userBackup = await getUser();
|
||||
if (userBackup == null) return;
|
||||
|
|
@ -117,13 +119,15 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
|
||||
|
||||
if (gUser.twonlySafeBackup!.lastBackupDone == null ||
|
||||
gUser.twonlySafeBackup!.lastBackupDone!
|
||||
.isAfter(clock.now().subtract(const Duration(days: 90)))) {
|
||||
gUser.twonlySafeBackup!.lastBackupDone!.isAfter(
|
||||
clock.now().subtract(const Duration(days: 90)),
|
||||
)) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
final lastHash =
|
||||
await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash);
|
||||
final lastHash = await storage.read(
|
||||
key: SecureStorageKeys.twonlySafeLastBackupHash,
|
||||
);
|
||||
|
||||
if (lastHash != null && !force) {
|
||||
if (backupHash == lastHash) {
|
||||
|
|
@ -155,8 +159,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
|
||||
Log.info('Backup files created.');
|
||||
|
||||
final encryptedBackupBytesFile =
|
||||
File(join(backupDir.path, 'twonly_safe.backup'));
|
||||
final encryptedBackupBytesFile = File(
|
||||
join(backupDir.path, 'twonly_safe.backup'),
|
||||
);
|
||||
|
||||
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:flutter_secure_storage/flutter_secure_storage.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/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
|
||||
|
|
@ -23,8 +23,10 @@ Future<void> recoverBackup(
|
|||
) async {
|
||||
final (backupId, encryptionKey) = await getMasterKey(password, username);
|
||||
|
||||
final backupServerUrl =
|
||||
await getTwonlySafeBackupUrlFromServer(backupId, server);
|
||||
final backupServerUrl = await getTwonlySafeBackupUrlFromServer(
|
||||
backupId,
|
||||
server,
|
||||
);
|
||||
|
||||
if (backupServerUrl == null) {
|
||||
Log.error('Could not create backup url');
|
||||
|
|
@ -87,8 +89,9 @@ Future<void> handleBackupData(
|
|||
plaintextBytes,
|
||||
);
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
|
||||
final originalDatabase = File(
|
||||
join(globalApplicationSupportDirectory, 'twonly.sqlite'),
|
||||
);
|
||||
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
|
||||
|
||||
const storage = FlutterSecureStorage();
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class KeyValueStore {
|
||||
static Future<File> _getFilePath(String key) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
return File('${directory.path}/keyvalue/$key.json');
|
||||
return File('$globalApplicationSupportDirectory/keyvalue/$key.json');
|
||||
}
|
||||
|
||||
static Future<void> delete(String key) async {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ void initLogger() {
|
|||
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) async {
|
||||
await _writeLogToFile(record);
|
||||
unawaited(_writeLogToFile(record));
|
||||
if (!kReleaseMode) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
|
|
@ -67,83 +67,123 @@ class Log {
|
|||
}
|
||||
|
||||
Future<String> loadLogFile() async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
return _protectFileAccess(() async {
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
if (logFile.existsSync()) {
|
||||
return logFile.readAsString();
|
||||
} else {
|
||||
return 'Log file does not exist.';
|
||||
}
|
||||
if (logFile.existsSync()) {
|
||||
return logFile.readAsString();
|
||||
} else {
|
||||
return 'Log file does not exist.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> readLast1000Lines() async {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final file = File('${dir.path}/app.log');
|
||||
if (!file.existsSync()) return '';
|
||||
final all = await file.readAsLines();
|
||||
final start = all.length > 1000 ? all.length - 1000 : 0;
|
||||
return all.sublist(start).join('\n');
|
||||
return _protectFileAccess(() async {
|
||||
final file = File('$globalApplicationSupportDirectory/app.log');
|
||||
if (!file.existsSync()) return '';
|
||||
final all = await file.readAsLines();
|
||||
final start = all.length > 1000 ? all.length - 1000 : 0;
|
||||
return all.sublist(start).join('\n');
|
||||
});
|
||||
}
|
||||
|
||||
Mutex sameProcessProtection = Mutex();
|
||||
final Mutex _logMutex = Mutex();
|
||||
|
||||
Future<T> _protectFileAccess<T>(Future<T> Function() action) async {
|
||||
return _logMutex.protect(() async {
|
||||
final lockFile = File('$globalApplicationSupportDirectory/app.log.lock');
|
||||
var lockAcquired = false;
|
||||
|
||||
while (!lockAcquired) {
|
||||
try {
|
||||
lockFile.createSync(exclusive: true);
|
||||
lockAcquired = true;
|
||||
} on FileSystemException catch (e) {
|
||||
final isExists = e is PathExistsException || e.osError?.errorCode == 17;
|
||||
if (!isExists) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
final stat = lockFile.statSync();
|
||||
if (stat.type != FileSystemEntityType.notFound) {
|
||||
final age = DateTime.now().difference(stat.modified).inMilliseconds;
|
||||
if (age > 1000) {
|
||||
lockFile.deleteSync();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
} catch (_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
if (lockAcquired) {
|
||||
try {
|
||||
if (lockFile.existsSync()) {
|
||||
lockFile.deleteSync();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _writeLogToFile(LogRecord record) async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
if (!logFile.existsSync()) {
|
||||
logFile.createSync(recursive: true);
|
||||
}
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
final logMessage =
|
||||
'${clock.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n';
|
||||
|
||||
// > Note that this does not actually lock the file for access. Also note that advisory locks are on a process level.
|
||||
// > This means that several isolates in the same process can obtain an exclusive lock on the same file.
|
||||
return sameProcessProtection.protect(() async {
|
||||
return _protectFileAccess(() async {
|
||||
if (!logFile.existsSync()) {
|
||||
logFile.createSync(recursive: true);
|
||||
}
|
||||
final raf = await logFile.open(mode: FileMode.writeOnlyAppend);
|
||||
|
||||
try {
|
||||
// Use FileLock.blockingExclusive to wait until the lock is available
|
||||
await raf.lock(FileLock.blockingExclusive);
|
||||
await raf.writeString(logMessage);
|
||||
await raf.flush();
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('Error during file access: $e');
|
||||
} finally {
|
||||
await raf.unlock();
|
||||
await raf.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cleanLogFile() async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
return _protectFileAccess(() async {
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
if (logFile.existsSync()) {
|
||||
final lines = await logFile.readAsLines();
|
||||
if (logFile.existsSync()) {
|
||||
final lines = await logFile.readAsLines();
|
||||
|
||||
if (lines.length <= 10000) return;
|
||||
if (lines.length <= 10000) return;
|
||||
|
||||
final removeCount = lines.length - 10000;
|
||||
final remaining = lines.sublist(removeCount, lines.length);
|
||||
final removeCount = lines.length - 10000;
|
||||
final remaining = lines.sublist(removeCount, lines.length);
|
||||
|
||||
final sink = logFile.openWrite()..writeAll(remaining, '\n');
|
||||
await sink.close();
|
||||
}
|
||||
final sink = logFile.openWrite()..writeAll(remaining, '\n');
|
||||
await sink.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> deleteLogFile() async {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final logFile = File('${directory.path}/app.log');
|
||||
return _protectFileAccess(() async {
|
||||
final logFile = File('$globalApplicationSupportDirectory/app.log');
|
||||
|
||||
if (logFile.existsSync()) {
|
||||
await logFile.delete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (logFile.existsSync()) {
|
||||
await logFile.delete();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
String _getCallerSourceCodeFilename() {
|
||||
|
|
@ -159,8 +199,11 @@ String _getCallerSourceCodeFilename() {
|
|||
lineNumber = parts.last.split(':')[1]; // Extract the line number
|
||||
} else {
|
||||
final firstLine = stackTraceString.split('\n')[0];
|
||||
fileName =
|
||||
firstLine.split('/').last.split(':').first; // Extract the file name
|
||||
fileName = firstLine
|
||||
.split('/')
|
||||
.last
|
||||
.split(':')
|
||||
.first; // Extract the file name
|
||||
lineNumber = firstLine.split(':')[1]; // Extract the line number
|
||||
}
|
||||
lineNumber = lineNumber.replaceAll(')', '');
|
||||
|
|
|
|||
|
|
@ -88,9 +88,8 @@ class MainCameraController {
|
|||
scannedUrl = null;
|
||||
try {
|
||||
await cameraController?.stopImageStream();
|
||||
} catch (e) {
|
||||
Log.warn(e);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
final cameraControllerTemp = cameraController;
|
||||
cameraController = null;
|
||||
// prevents: CameraException(Disposed CameraController, buildPreview() was called on a disposed CameraController.)
|
||||
|
|
@ -166,7 +165,8 @@ class MainCameraController {
|
|||
selectedCameraDetails.minAvailableZoom =
|
||||
await cameraController?.getMinZoomLevel() ?? 1;
|
||||
selectedCameraDetails
|
||||
..isZoomAble = selectedCameraDetails.maxAvailableZoom !=
|
||||
..isZoomAble =
|
||||
selectedCameraDetails.maxAvailableZoom !=
|
||||
selectedCameraDetails.minAvailableZoom
|
||||
..cameraLoaded = true
|
||||
..cameraId = cameraId;
|
||||
|
|
@ -323,8 +323,9 @@ class MainCameraController {
|
|||
customPaint = CustomPaint(painter: painter);
|
||||
|
||||
if (barcodes.isEmpty && timeSharedLinkWasSetWithQr != null) {
|
||||
if (timeSharedLinkWasSetWithQr!
|
||||
.isAfter(DateTime.now().subtract(const Duration(seconds: 2)))) {
|
||||
if (timeSharedLinkWasSetWithQr!.isAfter(
|
||||
DateTime.now().subtract(const Duration(seconds: 2)),
|
||||
)) {
|
||||
setSharedLinkForPreview(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -376,8 +377,8 @@ class MainCameraController {
|
|||
content: Text(
|
||||
globalRootScaffoldMessengerKey.currentContext?.lang
|
||||
.verifiedPublicKey(
|
||||
getContactDisplayName(contact),
|
||||
) ??
|
||||
getContactDisplayName(contact),
|
||||
) ??
|
||||
'',
|
||||
),
|
||||
duration: const Duration(seconds: 6),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
|||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.0.97+97
|
||||
version: 0.0.99+99
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ void main() {
|
|||
image:
|
||||
'https://files.mastodon.social/media_attachments/files/115/883/317/526/523/824/original/6fa7ef90ec68f1f1.jpg',
|
||||
vendor: Vendor.mastodonSocialMediaPosting,
|
||||
shareAction: 90,
|
||||
shareAction: 80,
|
||||
likeAction: 290,
|
||||
),
|
||||
LinkParserTest(
|
||||
|
|
|
|||
Loading…
Reference in a new issue