Gomuks SDK Rewrite #2
12 changed files with 156 additions and 199 deletions
wip go 2
commit
a012a2e762
6
build.yaml
Normal file
6
build.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
targets:
|
||||
$default:
|
||||
builders:
|
||||
json_serializable:
|
||||
options:
|
||||
field_rename: snake
|
||||
|
|
@ -1,37 +1,36 @@
|
|||
import "dart:convert";
|
||||
import "dart:ffi";
|
||||
import "dart:io";
|
||||
import "package:ffi/ffi.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:matrix/encryption.dart";
|
||||
import "package:nexus/controllers/database_controller.dart";
|
||||
import "package:nexus/helpers/extensions/gomuks_buffer.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:matrix/matrix.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) {
|
||||
// Convert the C string to Dart
|
||||
final cmdStr = command.cast<Utf8>().toDartString();
|
||||
print("Received event: $cmdStr (requestId=$requestId)");
|
||||
try {
|
||||
final muksEventType = command.cast<Utf8>().toDartString();
|
||||
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
|
||||
bool updateShouldNotify(
|
||||
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 {
|
||||
int build() {
|
||||
final handle = GomuksInit();
|
||||
ref.onDispose(() => GomuksDestroy(handle));
|
||||
|
||||
GomuksStart(
|
||||
handle,
|
||||
|
|
@ -40,80 +39,29 @@ class ClientController extends AsyncNotifier<Client> {
|
|||
),
|
||||
);
|
||||
|
||||
final client = Client(
|
||||
"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;
|
||||
return handle;
|
||||
}
|
||||
|
||||
Future<bool> setHomeserver(Uri homeserverUrl) async {
|
||||
final client = await future;
|
||||
(int requestId, Map<String, dynamic> response) sendCommand(
|
||||
String command,
|
||||
Map<String, dynamic> data,
|
||||
) {
|
||||
final responsePtr = calloc<GomuksBuffer>();
|
||||
try {
|
||||
await client.checkHomeserver(homeserverUrl);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> login(String username, String password) async {
|
||||
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,
|
||||
final requestId = GomuksSubmitCommand(
|
||||
state,
|
||||
command.toNativeUtf8().cast<Char>(),
|
||||
data.toGomuksBuffer(),
|
||||
responsePtr,
|
||||
);
|
||||
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;
|
||||
|
||||
return (requestId, responsePtr.ref.toJson());
|
||||
} finally {
|
||||
calloc.free(responsePtr);
|
||||
}
|
||||
}
|
||||
|
||||
static final provider = AsyncNotifierProvider<ClientController, Client>(
|
||||
static final provider = NotifierProvider<ClientController, int>(
|
||||
ClientController.new,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
41
lib/helpers/extensions/gomuks_buffer.dart
Normal file
41
lib/helpers/extensions/gomuks_buffer.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,11 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/controllers/client_controller.dart";
|
||||
import "package:nexus/controllers/shared_prefs_controller.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.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/loading.dart";
|
||||
import "package:window_manager/window_manager.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:dynamic_system_colors/dynamic_system_colors.dart";
|
||||
|
|
@ -103,37 +97,46 @@ class App extends ConsumerWidget {
|
|||
builder: (context) => ref
|
||||
.watch(SharedPrefsController.provider)
|
||||
.betterWhen(
|
||||
data: (_) => ref
|
||||
.watch(ClientController.provider)
|
||||
.betterWhen(
|
||||
data: (client) =>
|
||||
client.accessToken == null ? LoginPage() : ChatPage(),
|
||||
loading: () => Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Text(
|
||||
"Syncing...",
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
Loading(),
|
||||
],
|
||||
),
|
||||
),
|
||||
appBar: Appbar(
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => SettingsPage()),
|
||||
),
|
||||
icon: Icon(Icons.settings),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
data: (_) {
|
||||
final response = ref
|
||||
.watch(ClientController.provider.notifier)
|
||||
.sendCommand("login", {
|
||||
"homeserver_url": "federated.nexus",
|
||||
"username": "quadradical",
|
||||
"password": "Quadmarad1!",
|
||||
});
|
||||
debugPrint("$response");
|
||||
return Placeholder();
|
||||
},
|
||||
// .betterWhen(
|
||||
// data: (client) =>
|
||||
// client.accessToken == null ? LoginPage() : ChatPage(),
|
||||
// loading: () => Scaffold(
|
||||
// body: Center(
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// spacing: 16,
|
||||
// children: [
|
||||
// Text(
|
||||
// "Syncing...",
|
||||
// style: Theme.of(context).textTheme.headlineMedium,
|
||||
// ),
|
||||
// Loading(),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// appBar: Appbar(
|
||||
// actions: [
|
||||
// IconButton(
|
||||
// onPressed: () => Navigator.of(context).push(
|
||||
// MaterialPageRoute(builder: (_) => SettingsPage()),
|
||||
// ),
|
||||
// icon: Icon(Icons.settings),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
15
lib/models/client_state.dart
Normal file
15
lib/models/client_state.dart
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
enum RelationType { edit, reply }
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
18
lib/models/sync_status.dart
Normal file
18
lib/models/sync_status.dart
Normal 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 }
|
||||
Loading…
Add table
Add a link
Reference in a new issue