remove old database

This commit is contained in:
otsmr 2026-03-01 04:18:43 +01:00
parent de41111194
commit eb70d7119f
35 changed files with 1 additions and 13635 deletions

View file

@ -11,5 +11,4 @@ targets:
options:
databases:
twonly_db: lib/src/database/twonly.db.dart
twonly_database: lib/src/database/twonly_database_old.dart
schema_dir: lib/src/database/schemas

View file

@ -20,7 +20,6 @@ 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:twonly/src/views/settings/backup/setup_backup.view.dart';
import 'package:twonly/src/views/updates/62_database_migration.view.dart';
class App extends StatefulWidget {
const App({super.key});
@ -187,7 +186,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
late Widget child;
if (_showDatabaseMigration) {
child = const DatabaseMigrationView();
child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) {
if (gUser.twonlySafeBackup == null && !_skipBackup && kReleaseMode) {
child = SetupBackupView(

View file

@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/database/twonly_database_old.dart' as old;
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart';
@ -163,22 +162,6 @@ String substringBy(String string, int maxLength) {
return string;
}
String getContactDisplayNameOld(old.Contact user) {
var name = user.username;
if (user.nickName != null && user.nickName != '') {
name = user.nickName!;
} else if (user.displayName != null) {
name = user.displayName!;
}
if (user.deleted) {
name = applyStrikethrough(name);
}
if (name.length > 12) {
return '${name.substring(0, 12)}...';
}
return name;
}
String applyStrikethrough(String text) {
return text.split('').map((char) => '$char\u0336').join();
}

View file

@ -1,126 +0,0 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart';
import 'package:twonly/src/database/tables/signal_contact_signed_prekey.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'signal.dao.g.dart';
@DriftAccessor(
tables: [
SignalContactPreKeys,
SignalContactSignedPreKeys,
],
)
class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
// ignore: matching_super_parameters
SignalDao(super.db);
Future<void> deleteAllByContactId(int contactId) async {
await (delete(signalContactPreKeys)
..where((t) => t.contactId.equals(contactId)))
.go();
await (delete(signalContactSignedPreKeys)
..where((t) => t.contactId.equals(contactId)))
.go();
}
Future<void> deleteAllPreKeysByContactId(int contactId) async {
await (delete(signalContactPreKeys)
..where((t) => t.contactId.equals(contactId)))
.go();
}
// 1: Count the number of pre-keys by contact ID
Future<int> countPreKeysByContactId(int contactId) {
return (select(signalContactPreKeys)
..where((tbl) => tbl.contactId.equals(contactId)))
.get()
.then((rows) => rows.length);
}
// 2: Pop a pre-key by contact ID
Future<SignalContactPreKey?> popPreKeyByContactId(int contactId) async {
final preKey =
await ((select(signalContactPreKeys)..where((tbl) => tbl.contactId.equals(contactId)))
..limit(1))
.getSingleOrNull();
if (preKey != null) {
// remove the pre key...
await (delete(signalContactPreKeys)
..where(
(tbl) =>
tbl.contactId.equals(contactId) &
tbl.preKeyId.equals(preKey.preKeyId),
))
.go();
return preKey;
}
return null;
}
// 3: Insert multiple pre-keys
Future<void> insertPreKeys(
List<SignalContactPreKeysCompanion> preKeys,
) async {
for (final preKey in preKeys) {
try {
await into(signalContactPreKeys).insert(preKey);
} catch (e) {
Log.error('$e');
}
}
}
// 4: Get signed pre-key by contact ID
Future<SignalContactSignedPreKey?> getSignedPreKeyByContactId(int contactId) {
return (select(signalContactSignedPreKeys)
..where((tbl) => tbl.contactId.equals(contactId)))
.getSingleOrNull();
}
// 5: Insert or update signed pre-key by contact ID
Future<void> insertOrUpdateSignedPreKeyByContactId(
SignalContactSignedPreKeysCompanion signedPreKey,
) async {
await (delete(signalContactSignedPreKeys)
..where((t) => t.contactId.equals(signedPreKey.contactId.value)))
.go();
await into(signalContactSignedPreKeys).insert(signedPreKey);
}
Future<void> purgePreKeysFromContact(int contactId) async {
await (delete(signalContactPreKeys)
..where(
(t) => (t.contactId.equals(contactId)),
))
.go();
}
Future<void> purgeOutDatedPreKeys() async {
// other pre keys are valid 100 days
await (delete(signalContactPreKeys)
..where(
(t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract(
const Duration(days: 100),
),
)),
))
.go();
// own pre keys are valid for 180 days
await (delete(twonlyDB.signalPreKeyStores)
..where(
(t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract(
const Duration(days: 365),
),
)),
))
.go();
}
}

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 it is too large Load diff

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,42 +0,0 @@
import 'package:drift/drift.dart';
class Contacts extends Table {
IntColumn get userId => integer()();
TextColumn get username => text().unique()();
TextColumn get displayName => text().nullable()();
TextColumn get nickName => text().nullable()();
TextColumn get avatarSvg => text().nullable()();
IntColumn get myAvatarCounter => integer().withDefault(const Constant(0))();
BoolColumn get accepted => boolean().withDefault(const Constant(false))();
BoolColumn get requested => boolean().withDefault(const Constant(false))();
BoolColumn get blocked => boolean().withDefault(const Constant(false))();
BoolColumn get verified => boolean().withDefault(const Constant(false))();
BoolColumn get archived => boolean().withDefault(const Constant(false))();
BoolColumn get pinned => boolean().withDefault(const Constant(false))();
BoolColumn get deleted => boolean().withDefault(const Constant(false))();
BoolColumn get alsoBestFriend =>
boolean().withDefault(const Constant(false))();
IntColumn get deleteMessagesAfterXMinutes =>
integer().withDefault(const Constant(60 * 24))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))();
DateTimeColumn get lastMessageSend => dateTime().nullable()();
DateTimeColumn get lastMessageReceived => dateTime().nullable()();
DateTimeColumn get lastFlameCounterChange => dateTime().nullable()();
DateTimeColumn get lastFlameSync => dateTime().nullable()();
DateTimeColumn get lastMessageExchange =>
dateTime().withDefault(currentDateAndTime)();
IntColumn get flameCounter => integer().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {userId};
}

View file

@ -1,153 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
enum UploadState {
pending,
readyToUpload,
uploadTaskStarted,
receiverNotified,
}
@DataClassName('MediaUpload')
class MediaUploads extends Table {
IntColumn get mediaUploadId => integer().autoIncrement()();
TextColumn get state =>
textEnum<UploadState>().withDefault(Constant(UploadState.pending.name))();
TextColumn get metadata =>
text().map(const MediaUploadMetadataConverter()).nullable()();
/// exists in UploadState.addedToMessagesDb
TextColumn get messageIds => text().map(IntListTypeConverter()).nullable()();
TextColumn get encryptionData =>
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;
late DateTime messageSendAt;
late bool isVideo;
late bool mirrorVideo;
Map<String, dynamic> toJson() {
return {
'contactIds': contactIds,
'isRealTwonly': isRealTwonly,
'mirrorVideo': mirrorVideo,
'maxShowTime': maxShowTime,
'isVideo': isVideo,
'messageSendAt': messageSendAt.toIso8601String(),
};
}
}
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;
Map<String, dynamic> toJson() {
return {
'sha2Hash': sha2Hash,
'encryptionKey': encryptionKey,
'encryptionMac': encryptionMac,
'encryptionNonce': encryptionNonce,
};
}
}
// --- converters ----
class IntListTypeConverter extends TypeConverter<List<int>, String> {
@override
List<int> fromSql(String fromDb) {
return List<int>.from(jsonDecode(fromDb) as Iterable<dynamic>);
}
@override
String toSql(List<int> value) {
return json.encode(value);
}
}
class MediaUploadMetadataConverter
extends TypeConverter<MediaUploadMetadata, String>
with JsonTypeConverter2<MediaUploadMetadata, String, Map<String, Object?>> {
const MediaUploadMetadataConverter();
@override
MediaUploadMetadata fromSql(String fromDb) {
return fromJson(json.decode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(MediaUploadMetadata value) {
return json.encode(toJson(value));
}
@override
MediaUploadMetadata fromJson(Map<String, Object?> json) {
return MediaUploadMetadata.fromJson(json);
}
@override
Map<String, Object?> toJson(MediaUploadMetadata value) {
return value.toJson();
}
}
class MediaEncryptionDataConverter
extends TypeConverter<MediaEncryptionData, String>
with JsonTypeConverter2<MediaEncryptionData, String, Map<String, Object?>> {
const MediaEncryptionDataConverter();
@override
MediaEncryptionData fromSql(String fromDb) {
return fromJson(json.decode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(MediaEncryptionData value) {
return json.encode(toJson(value));
}
@override
MediaEncryptionData fromJson(Map<String, Object?> json) {
return MediaEncryptionData.fromJson(json);
}
@override
Map<String, Object?> toJson(MediaEncryptionData value) {
return value.toJson();
}
}

View file

@ -1,23 +0,0 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables_old/contacts_table.dart';
import 'package:twonly/src/database/tables_old/messages_table.dart';
@DataClassName('MessageRetransmission')
class MessageRetransmissions extends Table {
IntColumn get retransmissionId => integer().autoIncrement()();
IntColumn get contactId =>
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
IntColumn get messageId => integer()
.nullable()
.references(Messages, #messageId, onDelete: KeyAction.cascade)();
BlobColumn get plaintextContent => blob()();
BlobColumn get pushData => blob().nullable()();
BlobColumn get encryptedHash => blob().nullable()();
IntColumn get retryCount => integer().withDefault(const Constant(0))();
DateTimeColumn get lastRetry => dateTime().nullable()();
DateTimeColumn get acknowledgeByServerAt => dateTime().nullable()();
}

View file

@ -1,68 +0,0 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables_old/contacts_table.dart';
enum MessageKind {
textMessage,
storedMediaFile,
reopenedMedia,
media,
contactRequest,
profileChange,
rejectRequest,
acceptRequest,
flameSync,
opened,
ack,
pushKey,
requestPushKey,
receiveMediaError,
signalDecryptError
}
enum DownloadState {
pending,
downloading,
downloaded,
}
enum MediaRetransmitting {
none,
requested,
retransmitted,
}
@DataClassName('Message')
class Messages extends Table {
IntColumn get contactId => integer().references(Contacts, #userId)();
IntColumn get messageId => integer().autoIncrement()();
IntColumn get messageOtherId => integer().nullable()();
IntColumn get mediaUploadId => integer().nullable()();
IntColumn get mediaDownloadId => integer().nullable()();
IntColumn get responseToMessageId => integer().nullable()();
IntColumn get responseToOtherMessageId => integer().nullable()();
BoolColumn get acknowledgeByUser =>
boolean().withDefault(const Constant(false))();
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();
IntColumn get downloadState => intEnum<DownloadState>()
.withDefault(Constant(DownloadState.downloaded.index))();
BoolColumn get acknowledgeByServer =>
boolean().withDefault(const Constant(false))();
BoolColumn get errorWhileSending =>
boolean().withDefault(const Constant(false))();
TextColumn get mediaRetransmissionState => textEnum<MediaRetransmitting>()
.withDefault(Constant(MediaRetransmitting.none.name))();
TextColumn get kind => textEnum<MessageKind>()();
TextColumn get contentJson => text().nullable()();
DateTimeColumn get openedAt => dateTime().nullable()();
DateTimeColumn get sendAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
}

View file

@ -1,11 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalContactPreKey')
class SignalContactPreKeys extends Table {
IntColumn get contactId => integer()();
IntColumn get preKeyId => integer()();
BlobColumn get preKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {contactId, preKeyId};
}

View file

@ -1,12 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalContactSignedPreKey')
class SignalContactSignedPreKeys extends Table {
IntColumn get contactId => integer()();
IntColumn get signedPreKeyId => integer()();
BlobColumn get signedPreKey => blob()();
BlobColumn get signedPreKeySignature => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {contactId};
}

View file

@ -1,12 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalIdentityKeyStore')
class SignalIdentityKeyStores extends Table {
IntColumn get deviceId => integer()();
TextColumn get name => text()();
BlobColumn get identityKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {deviceId, name};
}

View file

@ -1,11 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalPreKeyStore')
class SignalPreKeyStores extends Table {
IntColumn get preKeyId => integer()();
BlobColumn get preKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {preKeyId};
}

View file

@ -1,10 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSenderKeyStore')
class SignalSenderKeyStores extends Table {
TextColumn get senderKeyName => text()();
BlobColumn get senderKey => blob()();
@override
Set<Column> get primaryKey => {senderKeyName};
}

View file

@ -1,12 +0,0 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSessionStore')
class SignalSessionStores extends Table {
IntColumn get deviceId => integer()();
TextColumn get name => text()();
BlobColumn get sessionRecord => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {deviceId, name};
}

View file

@ -1,202 +0,0 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/tables_old/contacts_table.dart';
import 'package:twonly/src/database/tables_old/media_uploads_table.dart';
import 'package:twonly/src/database/tables_old/message_retransmissions.dart';
import 'package:twonly/src/database/tables_old/messages_table.dart';
import 'package:twonly/src/database/tables_old/signal_contact_prekey_table.dart';
import 'package:twonly/src/database/tables_old/signal_contact_signed_prekey_table.dart';
import 'package:twonly/src/database/tables_old/signal_identity_key_store_table.dart';
import 'package:twonly/src/database/tables_old/signal_pre_key_store_table.dart';
import 'package:twonly/src/database/tables_old/signal_sender_key_store_table.dart';
import 'package:twonly/src/database/tables_old/signal_session_store_table.dart';
import 'package:twonly/src/database/twonly_database_old.steps.dart';
import 'package:twonly/src/utils/log.dart';
part 'twonly_database_old.g.dart';
// You can then create a database class that includes this table
@DriftDatabase(
tables: [
Contacts,
Messages,
MediaUploads,
SignalIdentityKeyStores,
SignalPreKeyStores,
SignalSenderKeyStores,
SignalSessionStores,
SignalContactPreKeys,
SignalContactSignedPreKeys,
MessageRetransmissions,
],
)
class TwonlyDatabaseOld extends _$TwonlyDatabaseOld {
TwonlyDatabaseOld([QueryExecutor? e])
: super(
e ?? _openConnection(),
);
// ignore: matching_super_parameters
TwonlyDatabaseOld.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 17;
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'twonly_database',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
);
}
@override
MigrationStrategy get migration {
return MigrationStrategy(
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
onUpgrade: stepByStep(
from1To2: (m, schema) async {
await m.addColumn(schema.messages, schema.messages.errorWhileSending);
},
from2To3: (m, schema) async {
await m.addColumn(schema.contacts, schema.contacts.archived);
await m.addColumn(
schema.contacts,
schema.contacts.deleteMessagesAfterXMinutes,
);
},
from3To4: (m, schema) async {
await m.createTable(schema.mediaUploads);
await m.alterTable(
TableMigration(
schema.mediaUploads,
columnTransformer: {
schema.mediaUploads.metadata:
schema.mediaUploads.metadata.cast<String>(),
},
),
);
},
from4To5: (m, schema) async {
await m.createTable(schema.mediaDownloads);
await m.addColumn(schema.messages, schema.messages.mediaDownloadId);
await m.addColumn(schema.messages, schema.messages.mediaUploadId);
},
from5To6: (m, schema) async {
await m.addColumn(schema.messages, schema.messages.mediaStored);
},
from6To7: (m, schema) async {
await m.addColumn(schema.contacts, schema.contacts.pinned);
},
from7To8: (m, schema) async {
await m.addColumn(schema.contacts, schema.contacts.alsoBestFriend);
await m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
},
from8To9: (m, schema) async {
await m.alterTable(
TableMigration(
schema.mediaUploads,
columnTransformer: {
schema.mediaUploads.metadata:
schema.mediaUploads.metadata.cast<String>(),
},
),
);
},
from9To10: (m, schema) async {
await m.createTable(schema.signalContactPreKeys);
await m.createTable(schema.signalContactSignedPreKeys);
await m.addColumn(schema.contacts, schema.contacts.deleted);
},
from10To11: (m, schema) async {
await m.createTable(schema.messageRetransmissions);
},
from11To12: (m, schema) async {
await m.addColumn(
schema.messageRetransmissions,
schema.messageRetransmissions.willNotGetACKByUser,
);
},
from12To13: (m, schema) async {
await m.dropColumn(
schema.messageRetransmissions,
'will_not_get_a_c_k_by_user',
);
},
from13To14: (m, schema) async {
await m.addColumn(
schema.messageRetransmissions,
schema.messageRetransmissions.encryptedHash,
);
},
from14To15: (m, schema) async {
await m.dropColumn(schema.mediaUploads, 'upload_tokens');
await m.dropColumn(schema.mediaUploads, 'already_notified');
await m.addColumn(
schema.messages,
schema.messages.mediaRetransmissionState,
);
},
from15To16: (m, schema) async {
await m.deleteTable('media_downloads');
},
from16To17: (m, schema) async {
await m.addColumn(
schema.messageRetransmissions,
schema.messageRetransmissions.lastRetry,
);
await m.addColumn(
schema.messageRetransmissions,
schema.messageRetransmissions.retryCount,
);
},
),
);
}
void markUpdated() {
notifyUpdates({TableUpdate.onTable(messages, kind: UpdateKind.update)});
notifyUpdates({TableUpdate.onTable(contacts, kind: UpdateKind.update)});
}
Future<void> printTableSizes() async {
final result = await customSelect(
'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name',
).get();
for (final row in result) {
final tableName = row.read<String>('name');
final tableSize = row.read<String>('size');
Log.info('Table: $tableName, Size: $tableSize bytes');
}
}
Future<void> deleteDataForTwonlySafe() async {
await delete(messages).go();
await delete(messageRetransmissions).go();
await delete(mediaUploads).go();
await update(contacts).write(
const ContactsCompanion(
avatarSvg: Value(null),
myAvatarCounter: Value(0),
),
);
await delete(signalContactPreKeys).go();
await delete(signalContactSignedPreKeys).go();
await (delete(signalPreKeyStores)
..where(
(t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract(
const Duration(days: 25),
),
)),
))
.go();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,426 +0,0 @@
import 'dart:collection' show HashSet;
import 'dart:convert';
import 'dart:io';
import 'package:clock/clock.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/database/twonly_database_old.dart'
show TwonlyDatabaseOld;
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
class DatabaseMigrationView extends StatefulWidget {
const DatabaseMigrationView({super.key});
@override
State<DatabaseMigrationView> createState() => _DatabaseMigrationViewState();
}
class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
bool _isMigrating = false;
bool _isMigratingFinished = false;
int _contactsMigrated = 0;
int _storedMediaFiles = 0;
Future<void> startMigration() async {
setState(() {
_isMigrating = true;
});
final oldDatabase = TwonlyDatabaseOld();
final oldContacts = await oldDatabase.contacts.select().get();
for (final oldContact in oldContacts) {
try {
if (oldContact.deleted) continue;
Uint8List? avatarSvg;
if (oldContact.avatarSvg != null) {
avatarSvg = Uint8List.fromList(
gzip.encode(utf8.encode(oldContact.avatarSvg!)),
);
}
await twonlyDB.contactsDao.insertContact(
ContactsCompanion(
userId: Value(oldContact.userId),
username: Value(oldContact.username),
displayName: Value(oldContact.displayName),
nickName: Value(oldContact.nickName),
avatarSvgCompressed: Value(avatarSvg),
senderProfileCounter: const Value(0),
accepted: Value(oldContact.accepted),
requested: Value(oldContact.requested),
blocked: Value(oldContact.blocked),
verified: Value(oldContact.verified),
createdAt: Value(oldContact.createdAt),
),
);
setState(() {
_contactsMigrated += 1;
});
await twonlyDB.groupsDao.createNewDirectChat(
oldContact.userId,
GroupsCompanion(
pinned: Value(oldContact.pinned),
archived: Value(oldContact.archived),
groupName: Value(getContactDisplayNameOld(oldContact)),
totalMediaCounter: Value(oldContact.totalMediaCounter),
alsoBestFriend: Value(oldContact.alsoBestFriend),
createdAt: Value(oldContact.createdAt),
lastFlameCounterChange: Value(oldContact.lastFlameCounterChange),
lastFlameSync: Value(oldContact.lastFlameSync),
lastMessageExchange: Value(oldContact.lastMessageExchange),
lastMessageReceived: Value(oldContact.lastMessageReceived),
lastMessageSend: Value(oldContact.lastMessageSend),
flameCounter: Value(oldContact.flameCounter),
maxFlameCounter: Value(oldContact.flameCounter),
maxFlameCounterFrom: Value(clock.now()),
),
);
} catch (e) {
Log.error(e);
}
}
final folders = ['memories', 'send', 'received'];
final alreadyCopied = HashSet();
for (final folder in folders) {
final memoriesPath = Directory(
join(
(await getApplicationSupportDirectory()).path,
'media',
folder,
),
);
if (memoriesPath.existsSync()) {
final files = memoriesPath.listSync();
for (final file in files) {
try {
if (file.path.contains('thumbnail')) continue;
late MediaType type;
if (file.path.contains('mp4')) {
type = MediaType.video;
} else if (file.path.contains('png')) {
type = MediaType.image;
} else {
continue;
}
final bytes = File(file.path).readAsBytesSync();
final digest = (await Sha256().hash(bytes)).bytes;
if (alreadyCopied.contains(digest)) {
continue;
}
alreadyCopied.add(digest);
final stat = FileStat.statSync(file.path);
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(type),
createdAt: Value(stat.modified),
stored: const Value(true),
),
);
final mediaService = MediaFileService(mediaFile!);
File(file.path).copySync(mediaService.storedPath.path);
setState(() {
_storedMediaFiles += 1;
});
} catch (e) {
Log.error(e);
}
}
}
}
final oldContactPreKeys =
await oldDatabase.signalContactPreKeys.select().get();
for (final oldContactPreKey in oldContactPreKeys) {
try {
await twonlyDB
.into(twonlyDB.signalContactPreKeys)
.insert(SignalContactPreKey.fromJson(oldContactPreKey.toJson()));
} catch (e) {
Log.error(e);
}
}
final oldSignalSessionStores =
await oldDatabase.signalSessionStores.select().get();
for (final oldSignalSessionStore in oldSignalSessionStores) {
try {
await twonlyDB.into(twonlyDB.signalSessionStores).insert(
SignalSessionStore.fromJson(oldSignalSessionStore.toJson()),
);
} catch (e) {
Log.error(e);
}
}
final oldSignalSenderKeyStores =
await oldDatabase.signalSenderKeyStores.select().get();
for (final oldSignalSenderKeyStore in oldSignalSenderKeyStores) {
try {
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
SignalSenderKeyStore.fromJson(oldSignalSenderKeyStore.toJson()),
);
} catch (e) {
Log.error(e);
}
}
final oldSignalPreyKeyStores =
await oldDatabase.signalPreKeyStores.select().get();
for (final oldSignalPreyKeyStore in oldSignalPreyKeyStores) {
try {
await twonlyDB
.into(twonlyDB.signalPreKeyStores)
.insert(SignalPreKeyStore.fromJson(oldSignalPreyKeyStore.toJson()));
} catch (e) {
Log.error(e);
}
}
final oldSignalIdentityKeyStores =
await oldDatabase.signalIdentityKeyStores.select().get();
for (final oldSignalIdentityKeyStore in oldSignalIdentityKeyStores) {
try {
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert(
SignalIdentityKeyStore.fromJson(
oldSignalIdentityKeyStore.toJson(),
),
);
} catch (e) {
Log.error(e);
}
}
final oldSignalContactSignedPreKeys =
await oldDatabase.signalContactSignedPreKeys.select().get();
for (final oldSignalContactSignedPreKey in oldSignalContactSignedPreKeys) {
try {
await twonlyDB.into(twonlyDB.signalContactSignedPreKeys).insert(
SignalContactSignedPreKey.fromJson(
oldSignalContactSignedPreKey.toJson(),
),
);
} catch (e) {
Log.error(e);
}
}
await updateUserdata((u) {
u.appVersion = 62;
return u;
});
setState(() {
_isMigratingFinished = true;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(12),
child: _isMigratingFinished
? ListView(
children: [
const SizedBox(height: 40),
const Text(
'Deine Daten wurden migriert.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
...[
'$_contactsMigrated Kontakte',
'$_storedMediaFiles gespeicherte Mediendateien',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 17),
),
),
const SizedBox(height: 40),
const Text(
'Sollte du feststellen, dass es bei der Migration Fehler gab, zum Beispiel, dass Bilder fehlen, dann melde dies bitte über das Feedback-Formular. Du hast dafür eine Woche Zeit, danach werden deine alte Daten unwiederruflich gelöscht.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
const SizedBox(height: 30),
FilledButton(
onPressed: () {
Restart.restartApp(
notificationTitle: 'Deine Daten wurden migriert.',
notificationBody: 'Click here to open the app again',
);
},
child: const Text(
'App neu starten',
),
),
],
)
: _isMigrating
? ListView(
children: [
const SizedBox(height: 40),
const Text(
'Deine Daten werden migriert.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
const Center(
child: SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(),
),
),
const SizedBox(height: 40),
const Text(
'twonly während der Migration NICHT schließen!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, color: Colors.red),
),
const SizedBox(height: 40),
const Text(
'Aktueller Status',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20),
),
...[
'$_contactsMigrated Kontakte',
'$_storedMediaFiles gespeicherte Mediendateien',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 17),
),
),
],
)
: ListView(
children: [
const SizedBox(height: 40),
const Text(
'twonly. Besser als je zuvor.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 30),
const Text(
'Das sind die neuen Features.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 10),
...[
'Gruppen',
'Nachrichten bearbeiten & löschen',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 17),
),
),
const Text(
'Technische Neuerungen',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 17),
),
...[
'Client-to-Client (C2C) Protokoll umgestellt auf ProtoBuf.',
'Verwendung von UUIDs in der Datenbank',
'Von Grund auf neues Datenbank-Schema',
'Verbesserung der Zuverlässigkeit von C2C Nachrichten',
'Verbesserung von Videos',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 10),
),
),
const SizedBox(height: 50),
const Text(
'Was bedeutet das für dich?',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20),
),
const Text(
'Aufgrund der technischen Umstellung müssen wir deine alte Datenbank sowie deine gespeicherten Bilder migieren. Durch die Migration gehen einige Informationen verloren.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 10),
const Text(
'Was nach der Migration erhalten bleibt.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15),
),
...[
'Gespeicherte Bilder',
'Kontakte',
'Flammen',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
),
const SizedBox(height: 10),
const Text(
'Was durch die Migration verloren geht.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 15, color: Colors.red),
),
...[
'Text-Nachrichten und Reaktionen',
'Alles, was gesendet wurde, aber noch nicht empfangen wurde, wie Nachrichten und Bilder.',
].map(
(e) => Text(
e,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
),
const SizedBox(height: 30),
FilledButton(
onPressed: startMigration,
child: const Text(
'Jetzt starten',
),
),
],
),
),
);
}
}