import "dart:ffi"; import "dart:io"; import "dart:isolate"; import "dart:math"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:ffi/ffi.dart"; import "package:flutter/foundation.dart"; import "package:nexus/controllers/account_data.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/init_complete.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/space_edges.dart"; import "package:nexus/controllers/sync_status.dart"; import "package:nexus/controllers/top_level_spaces.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/main.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/oauth_auth_code_response.dart"; import "package:nexus/models/open_graph_data.dart"; import "package:nexus/models/paginate.dart"; import "package:nexus/models/requests/download_media.dart"; import "package:nexus/models/requests/get_event.dart"; import "package:nexus/models/requests/get_related_events.dart"; import "package:nexus/models/requests/get_room_state.dart"; import "package:nexus/models/requests/join_room.dart"; import "package:nexus/models/profile_response.dart"; import "package:nexus/models/requests/oauth/exchange_token.dart"; import "package:nexus/models/requests/oauth/get_auth_url.dart"; import "package:nexus/models/requests/oauth/register_client.dart"; import "package:nexus/models/requests/paginate.dart"; import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/requests/report.dart"; import "package:nexus/models/requests/send_event.dart"; import "package:nexus/models/requests/send_message.dart"; import "package:nexus/models/requests/set_account_data.dart"; import "package:nexus/models/requests/set_membership.dart"; import "package:nexus/models/requests/set_state.dart"; import "package:nexus/models/requests/upload_media.dart"; import "package:nexus/models/room.dart"; import "package:nexus/models/spec_versions_response.dart"; import "package:nexus/models/sync_data.dart"; import "package:nexus/src/third_party/gomuks.g.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:path_provider/path_provider.dart"; class ClientController extends AsyncNotifier { @override Future build() async { final Pointer root; if (Platform.isAndroid || Platform.isIOS) { final dir = await getApplicationSupportDirectory(); root = "${dir.path}/gomuks".toNativeUtf8().cast(); } else { root = nullptr.cast(); } final handle = GomuksInit(root); final callable = NativeCallable< Void Function(Pointer, Int64, GomuksOwnedBuffer) >.listener(( Pointer command, int requestId, GomuksOwnedBuffer data, ) { try { final muksEventType = command.cast().toDartString(); debugPrint("Handling $muksEventType..."); final decodedMuksEvent = data.toJson(); switch (muksEventType) { case "client_state": ref .watch(ClientStateController.provider.notifier) .set(.fromJson(decodedMuksEvent)); break; case "sync_status": ref .watch(SyncStatusController.provider.notifier) .set(.fromJson(decodedMuksEvent)); break; case "init_complete": ref.watch(InitCompleteController.provider.notifier).complete(); break; case "send_complete": final event = Event.fromJson(decodedMuksEvent["event"]); ref .watch(RoomsController.provider.notifier) .update( .new({ event.roomId: .new(events: .new({event.rowId: event})), }), .new(), ); break; case "sync_complete": final syncData = SyncData.fromJson(decodedMuksEvent); final roomProvider = RoomsController.provider; final accountDataProvider = AccountDataController.provider; if (syncData.clearState) { ref.invalidate(roomProvider); ref.invalidate(accountDataProvider); } ref .watch(roomProvider.notifier) .update(syncData.rooms, syncData.leftRooms); ref .watch(accountDataProvider.notifier) .update(syncData.accountData); if (syncData.topLevelSpaces != null) { ref .watch(TopLevelSpacesController.provider.notifier) .set(syncData.topLevelSpaces!); } if (syncData.spaceEdges != null) { ref .watch(SpaceEdgesController.provider.notifier) .set(syncData.spaceEdges!); } // ref // .watch(SyncStatusController.provider.notifier) // .set(SyncStatus.fromJson(decodedMuksEvent)); break; default: debugPrint("Unhandled event: $muksEventType"); } debugPrint("Finished handling $muksEventType..."); } catch (error, stackTrace) { if (kDebugMode) { debugPrintStack(stackTrace: stackTrace, label: error.toString()); rethrow; } else { showError(error, stackTrace); } } }); ref.onDispose(() => GomuksDestroy(handle)); ref.onDispose(callable.close); final errorCode = GomuksStart(handle, callable.nativeFunction); if (errorCode == 0) return handle; throw Exception("GomuksStart returned error code $errorCode"); } Future _sendCommand( String command, [ Map data = const {}, ]) async { final bufferPointer = data.toGomuksBufferPtr(); final handle = await future; final response = await Isolate.run( () => GomuksSubmitCommand( handle, command.toNativeUtf8().cast(), bufferPointer.ref, ), ); calloc.free(bufferPointer); final json = response.buf.toJson(); if (response.command.cast().toDartString() == "error") { throw json; } return json; } Future redactEvent(RedactEventRequest report) => _sendCommand("redact_event", report.toJson()); Future sendMessage(SendMessageRequest request) async => Event.fromJson(await _sendCommand("send_message", request.toJson())); Future sendEvent(SendEventRequest request) async { final json = request.toJson(); final content = request.content.toJson(); return Event.fromJson( await _sendCommand("send_event", { ...json, "content": { ...content, "m.relates_to": { ...((content["m.relates_to"] as Map?) ?? {}), "event_id": request.relatesTo, "rel_type": request.relationType, }, }, }), ); } Future setState(SetStateRequest request) async => await _sendCommand("set_state", request.toJson()); Future verify(String recoveryKey) async { try { await _sendCommand("verify", {"recovery_key": recoveryKey}); return null; } catch (error) { return error.toString(); } } Future joinRoom(JoinRoomRequest request) async { final response = await _sendCommand("join_room", request.toJson()); return response["room_id"]; } Future leaveRoom(Room room) async { if (room.metadata == null) return; await _sendCommand("leave_room", {"room_id": room.metadata!.id}); } // (await _sendCommand("get_event_context", { // "room_id": request.roomId, // "event_id": r"$OqZT4NuTj0J1-771IOEEWRI4XdumRNu6ighlvO3K3gc", // })); Future> getRoomState(GetRoomStateRequest request) async { Future getState(GetRoomStateRequest request) async => (await _sendCommand("get_room_state", request.toJson())) as List?; final response = await getState(request); return .new( (response ?? await getState(request.copyWith(refetch: true)) ?? []).map( (event) => .fromJson(event), ), ); } Future?> getRelatedEvents( GetRelatedEventsRequest request, ) async { final response = (await _sendCommand("get_related_events", request.toJson())) as List?; return .new(response?.map((event) => .fromJson(event))); } Future getEvent(GetEventRequest request) async { final json = await _sendCommand("get_event", request.toJson()); return json == null ? null : .fromJson(json); } Future getUrlPreview(Uri url) async => .fromJson(await _sendCommand("get_url_preview", {"url": url.toString()})); Future paginate(PaginateRequest request) async => .fromJson(await _sendCommand("paginate", request.toJson())); Future getProfile(String userId) async => .fromJson(await _sendCommand("get_profile", {"user_id": userId})); Future reportEvent(ReportRequest request) => _sendCommand("report_event", request.toJson()); Future setMembership(SetMembershipRequest request) => _sendCommand("set_membership", request.toJson()); Future setAccountData(SetAccountDataRequest request) => _sendCommand("set_account_data", request.toJson()); Future uploadMedia(UploadMediaRequest request) async => .fromJson(await _sendCommand("upload_media", request.toJson())); Future downloadMedia(DownloadMediaRequest request) async => .new((await _sendCommand("download_media", request.toJson()))["path"]); Future logout() => _sendCommand("logout"); Future markRead(Room room) async { final eventRowId = room.timeline[room.timeline.keys.reduce(max)]; final event = eventRowId == null ? null : room.events[eventRowId]; if (event == null || room.metadata == null) return; await _sendCommand("mark_read", { "room_id": room.metadata!.id, "receipt_type": "m.read", "event_id": event.eventId, }); } Future registerClient(OAuthRegisterClientRequest request) async => (await _sendCommand( "oauth_register_client", request.toJson(), ))["client_id"]; Future getAuthUrl(OAuthGetAuthUrl request) async => .fromJson( await _sendCommand("oauth_get_authorization_url", request.toJson()), ); Future exchangeToken(OAuthExchangeTokenRequest request) async => await _sendCommand("oauth_exchange_token", request.toJson()); Future getSpecVersions() async => .fromJson(await _sendCommand("get_versions")); Future discoverHomeserver(Uri homeserver) async { try { final response = await _sendCommand("discover_homeserver", { "user_id": "@fake-user:${homeserver.host}", }); return Uri.parse(response["m.homeserver"]?["base_url"]); } catch (error) { return null; } } static final provider = AsyncNotifierProvider( ClientController.new, ); }