Gomuks SDK Rewrite #2

Closed
Henry-Hiles wants to merge 34 commits from go into main
12 changed files with 156 additions and 199 deletions
Showing only changes of commit a012a2e762 - Show all commits

wip go 2

Henry Hiles 2026-01-23 13:47:21 +00:00
No known key found for this signature in database

6
build.yaml Normal file
View file

@ -0,0 +1,6 @@
targets:
$default:
builders:
json_serializable:
options:
field_rename: snake

View file

@ -1,37 +1,36 @@
import "dart:convert"; import "dart:convert";
import "dart:ffi"; import "dart:ffi";
import "dart:io";
import "package:ffi/ffi.dart"; import "package:ffi/ffi.dart";
import "package:flutter/foundation.dart"; import "package:flutter/foundation.dart";
import "package:matrix/encryption.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/controllers/database_controller.dart"; import "package:nexus/models/client_state.dart";
import "package:nexus/models/sync_status.dart";
import "package:nexus/src/third_party/gomuks.g.dart"; import "package:nexus/src/third_party/gomuks.g.dart";
import "package:matrix/matrix.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/secure_storage_controller.dart";
import "package:nexus/models/session_backup.dart";
void gomuksCallback(Pointer<Char> command, int requestId, GomuksBuffer data) { void gomuksCallback(Pointer<Char> command, int requestId, GomuksBuffer data) {
// Convert the C string to Dart try {
final cmdStr = command.cast<Utf8>().toDartString(); final muksEventType = command.cast<Utf8>().toDartString();
print("Received event: $cmdStr (requestId=$requestId)"); final Map<String, dynamic> decodedMuksEvent = data.toJson();
// Optionally inspect 'data' if you need switch (muksEventType) {
case "client_state":
final clientState = ClientState.fromJson(decodedMuksEvent);
debugPrint("Received event: $clientState");
case "sync_status":
final syncStatus = SyncStatus.fromJson(decodedMuksEvent);
debugPrint("Received event: $syncStatus");
}
} catch (error, stackTrace) {
debugPrintStack(stackTrace: stackTrace, label: error.toString());
}
} }
class ClientController extends AsyncNotifier<Client> { class ClientController extends Notifier<int> {
@override @override
bool updateShouldNotify( int build() {
AsyncValue<Client> previous,
AsyncValue<Client> next,
) =>
previous.hasValue != next.hasValue ||
previous.value?.accessToken != next.value?.accessToken;
static const sessionBackupKey = "sessionBackup";
@override
Future<Client> build() async {
final handle = GomuksInit(); final handle = GomuksInit();
ref.onDispose(() => GomuksDestroy(handle));
GomuksStart( GomuksStart(
handle, handle,
@ -40,80 +39,29 @@ class ClientController extends AsyncNotifier<Client> {
), ),
); );
final client = Client( return handle;
"nexus",
logLevel: kReleaseMode ? Level.warning : Level.verbose,
importantStateEvents: {"im.ponies.room_emotes"},
supportedLoginTypes: {AuthenticationTypes.password},
verificationMethods: {KeyVerificationMethod.emoji},
database: await MatrixSdkDatabase.init(
"nexus",
database: await ref.watch(DatabaseController.provider.future),
),
);
final backupJson = await ref
.watch(SecureStorageController.provider.notifier)
.get(sessionBackupKey);
if (backupJson != null) {
final backup = SessionBackup.fromJson(json.decode(backupJson));
await client.init(
waitForFirstSync: false,
newToken: backup.accessToken,
newHomeserver: backup.homeserver,
newUserID: backup.userID,
newDeviceID: backup.deviceID,
newDeviceName: backup.deviceName,
);
} }
return client; (int requestId, Map<String, dynamic> response) sendCommand(
} String command,
Map<String, dynamic> data,
Future<bool> setHomeserver(Uri homeserverUrl) async { ) {
final client = await future; final responsePtr = calloc<GomuksBuffer>();
try { try {
await client.checkHomeserver(homeserverUrl); final requestId = GomuksSubmitCommand(
return true; state,
} catch (_) { command.toNativeUtf8().cast<Char>(),
return false; data.toGomuksBuffer(),
responsePtr,
);
return (requestId, responsePtr.ref.toJson());
} finally {
calloc.free(responsePtr);
} }
} }
Future<bool> login(String username, String password) async { static final provider = NotifierProvider<ClientController, int>(
final client = await future;
try {
final deviceName = "Nexus Client login on ${Platform.localHostname}";
final details = await MatrixApi(homeserver: client.homeserver).login(
LoginType.mLoginPassword,
initialDeviceDisplayName: deviceName,
identifier: AuthenticationUserIdentifier(user: username),
password: password,
);
await ref
.watch(SecureStorageController.provider.notifier)
.set(
sessionBackupKey,
json.encode(
SessionBackup(
accessToken: details.accessToken,
homeserver: client.homeserver!,
userID: details.userId,
deviceID: details.deviceId,
deviceName: deviceName,
).toJson(),
),
);
ref.invalidateSelf(asReload: true);
return true;
} catch (_) {
return false;
}
}
static final provider = AsyncNotifierProvider<ClientController, Client>(
ClientController.new, ClientController.new,
); );
} }

View file

@ -0,0 +1,41 @@
import "dart:convert";
import "dart:ffi";
import "dart:typed_data";
import "package:ffi/ffi.dart";
import "package:nexus/src/third_party/gomuks.g.dart";
extension GomuksBufferX on GomuksBuffer {
/// Safely converts the Go buffer into a Dart `Uint8List`
Uint8List toBytes() {
if (base == nullptr || length <= 0) return Uint8List(0);
return base.asTypedList(length);
}
/// Decodes the bytes as JSON
Map<String, dynamic> toJson() {
final bytes = toBytes();
if (bytes.isEmpty) return {};
return jsonDecode(utf8.decode(bytes));
}
}
extension JsonToGomuksBuffer on Map<String, dynamic> {
GomuksBuffer toGomuksBuffer() {
final jsonString = json.encode(this);
final bytes = utf8.encode(jsonString);
final dataPtr = calloc<Uint8>(bytes.length);
dataPtr.asTypedList(bytes.length).setAll(0, bytes);
final bufPtr = calloc<GomuksBuffer>();
bufPtr.ref.base = dataPtr;
bufPtr.ref.length = bytes.length;
final bufByValue = bufPtr.ref;
calloc.free(bufPtr);
return bufByValue;
}
}

View file

@ -1,17 +1,11 @@
import "dart:io"; import "dart:io";
import "package:flutter/foundation.dart"; import "package:flutter/foundation.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/shared_prefs_controller.dart"; import "package:nexus/controllers/shared_prefs_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart";
import "package:nexus/pages/chat_page.dart";
import "package:nexus/pages/login_page.dart";
import "package:nexus/pages/settings_page.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/loading.dart";
import "package:window_manager/window_manager.dart"; import "package:window_manager/window_manager.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:dynamic_system_colors/dynamic_system_colors.dart"; import "package:dynamic_system_colors/dynamic_system_colors.dart";
@ -103,37 +97,46 @@ class App extends ConsumerWidget {
builder: (context) => ref builder: (context) => ref
.watch(SharedPrefsController.provider) .watch(SharedPrefsController.provider)
.betterWhen( .betterWhen(
data: (_) => ref data: (_) {
.watch(ClientController.provider) final response = ref
.betterWhen( .watch(ClientController.provider.notifier)
data: (client) => .sendCommand("login", {
client.accessToken == null ? LoginPage() : ChatPage(), "homeserver_url": "federated.nexus",
loading: () => Scaffold( "username": "quadradical",
body: Center( "password": "Quadmarad1!",
child: Column( });
mainAxisSize: MainAxisSize.min, debugPrint("$response");
spacing: 16, return Placeholder();
children: [ },
Text( // .betterWhen(
"Syncing...", // data: (client) =>
style: Theme.of(context).textTheme.headlineMedium, // client.accessToken == null ? LoginPage() : ChatPage(),
), // loading: () => Scaffold(
Loading(), // body: Center(
], // child: Column(
), // mainAxisSize: MainAxisSize.min,
), // spacing: 16,
appBar: Appbar( // children: [
actions: [ // Text(
IconButton( // "Syncing...",
onPressed: () => Navigator.of(context).push( // style: Theme.of(context).textTheme.headlineMedium,
MaterialPageRoute(builder: (_) => SettingsPage()), // ),
), // Loading(),
icon: Icon(Icons.settings), // ],
), // ),
], // ),
), // appBar: Appbar(
), // actions: [
), // IconButton(
// onPressed: () => Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => SettingsPage()),
// ),
// icon: Icon(Icons.settings),
// ),
// ],
// ),
// ),
// ),
), ),
), ),
), ),

View file

@ -0,0 +1,15 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "client_state.freezed.dart";
part "client_state.g.dart";
@freezed
abstract class ClientState with _$ClientState {
const factory ClientState({
required bool isInitialized,
required bool isLoggedIn,
required bool isVerified,
}) = _ClientState;
factory ClientState.fromJson(Map<String, Object?> json) =>
_$ClientStateFromJson(json);
}

View file

@ -1,13 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
import "package:matrix/matrix.dart";
part "full_room.freezed.dart";
@freezed
abstract class FullRoom with _$FullRoom {
const FullRoom._();
const factory FullRoom({
required Room roomData,
required String title,
required Uri? avatar,
}) = _FullRoom;
}

View file

@ -1,12 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "homeserver.freezed.dart";
@freezed
abstract class Homeserver with _$Homeserver {
const factory Homeserver({
required String name,
required String description,
required Uri url,
required String iconUrl,
}) = _Homeserver;
}

View file

@ -1,11 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "image_data.freezed.dart";
@freezed
abstract class ImageData with _$ImageData {
const factory ImageData({
required String uri,
required int? height,
required int? width,
}) = _ImageData;
}

View file

@ -1 +0,0 @@
enum RelationType { edit, reply }

View file

@ -1,17 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "session_backup.freezed.dart";
part "session_backup.g.dart";
@freezed
abstract class SessionBackup with _$SessionBackup {
const factory SessionBackup({
required String accessToken,
required Uri homeserver,
required String userID,
required String deviceID,
required String deviceName,
}) = _SessionBackup;
factory SessionBackup.fromJson(Map<String, Object?> json) =>
_$SessionBackupFromJson(json);
}

View file

@ -1,20 +0,0 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/widgets.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:matrix/matrix.dart";
import "package:nexus/models/full_room.dart";
part "space.freezed.dart";
@freezed
abstract class Space with _$Space {
const Space._();
const factory Space({
required String title,
required String id,
required IList<FullRoom> children,
required Client client,
Room? roomData,
Uri? avatar,
IconData? icon,
}) = _Space;
}

View file

@ -0,0 +1,18 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "sync_status.freezed.dart";
part "sync_status.g.dart";
@freezed
abstract class SyncStatus with _$SyncStatus {
const factory SyncStatus({
required Type type,
required int errorCount,
required int lastSync,
}) = _SyncStatus;
factory SyncStatus.fromJson(Map<String, Object?> json) =>
_$SyncStatusFromJson(json);
}
@JsonEnum(fieldRename: FieldRename.snake)
enum Type { ok, waiting, erroring, permanentlyFailed }