import "dart:async"; 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/cupertino.dart"; import "package:intl/intl.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/models/capabilities.dart"; import "package:nexus/models/content/message.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/event_context.dart"; import "package:nexus/models/gomuks_config.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/paginate_manual.dart"; import "package:nexus/models/requests/deregister_pusher.dart"; import "package:nexus/models/requests/download_media.dart"; import "package:nexus/models/requests/get_event.dart"; import "package:nexus/models/requests/get_event_context.dart"; import "package:nexus/models/requests/get_mentions.dart"; import "package:nexus/models/requests/get_related_events.dart"; import "package:nexus/models/requests/get_room_state.dart"; import "package:nexus/models/requests/get_room_summary.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/paginate_manual.dart"; import "package:nexus/models/requests/redact_event.dart"; import "package:nexus/models/requests/register_pusher.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/room_metadata.dart"; import "package:nexus/models/room_summary.dart"; import "package:nexus/models/spec_versions_response.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 { debugPrint("Setting Gomuks env..."); if (Platform.isAndroid || Platform.isIOS) { final env = { "GOMUKS_ROOT": (await getApplicationSupportDirectory()).path, "GOMUKS_CACHE_HOME": (await getApplicationCacheDirectory()).path, }; for (final MapEntry(:key, :value) in env.entries) { final keyPtr = key.toNativeUtf8().cast(); final valuePtr = value.toNativeUtf8().cast(); try { GomuksSetEnv(keyPtr, valuePtr); } finally { calloc ..free(keyPtr) ..free(valuePtr); } } } debugPrint("Initializing Gomuks..."); final handle = await Isolate.run(() { final bufferPointer = GomuksConfig( matrix: .new( initialDeviceDisplayName: "Nexus on ${toBeginningOfSentenceCase(Platform.operatingSystem)}", ), ).toJson().toGomuksBufferPtr(); try { return GomuksInit(bufferPointer.ref); } finally { calloc ..free(bufferPointer.ref.base) ..free(bufferPointer); } }); ref.onDispose(() => GomuksDestroy(handle)); return handle; } Future callGomuksMethod( Map data, FutureOr Function(int handle, GomuksBorrowedBuffer data) callback, ) async { final bufferPointer = data.toGomuksBufferPtr(); try { final handle = await future; final response = await Isolate.run( () => callback(handle, bufferPointer.ref), ); final json = response.buf.toJsonAndFree(); if (response.command.cast().toDartString() == "error") { throw json; } return json; } finally { calloc ..free(bufferPointer.ref.base) ..free(bufferPointer); } } Future<(Event, RoomMetadata)?> handlePush(Map data) async { final response = await callGomuksMethod( data, (handle, data) async => GomuksHandlePush(handle, data), ); return response == null ? null : ( Event.fromJson(response["event"]), RoomMetadata.fromJson(response["room"]), ); } Future _sendCommand( String command, [ Map data = const {}, ]) => callGomuksMethod(data, (handle, data) { final commandPointer = command.toNativeUtf8().cast(); try { return GomuksSubmitCommand(handle, commandPointer, data); } finally { calloc.free(commandPointer); } }); 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 .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 => (await _sendCommand("join_room", request.toJson()))["room_id"]; Future getRoomSummary(GetRoomSummary request) async => .fromJson(await _sendCommand("get_room_summary", request.toJson())); Future leaveRoom(Room room) async { if (room.metadata == null) return; await _sendCommand("leave_room", {"room_id": room.metadata!.id}); } 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> getMentions(GetMentionsRequest request) async => .new( // TODO: Handle `related_events` ((await _sendCommand("get_mentions", request.toJson()))["events"] as List) .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 paginateManual(PaginateManualRequest request) async => .fromJson(await _sendCommand("paginate_manual", request.toJson())); Future getEventContext(GetEventContextRequest request) async => .fromJson(await _sendCommand("get_event_context", request.toJson())); Future getProfile(String userId) async { try { return .fromJson(await _sendCommand("get_profile", {"user_id": userId})); } catch (_) { return .new(profile: .new(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 registerPusher(RegisterPusherRequest request) => _sendCommand("register_homeserver_push", request.toJson()); Future deregisterPusher(DeregisterPusherRequest request) => _sendCommand("register_homeserver_push", { ...request.toJson(), "kind": null, }); 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 { if (room.timeline.isEmpty || room.metadata == null) return; final eventRowId = room.timeline[room.timeline.keys.reduce(max)]; final event = eventRowId == null ? null : room.events[eventRowId]; if (event == 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 getCapabilities() async => .fromJson((await _sendCommand("get_capabilities"))["capabilities"]); Future discoverHomeserver(Uri homeserver) async { try { final response = await _sendCommand("discover_homeserver", { "user_id": "@fake-user:${homeserver.authority}", }); return Uri.parse(response["m.homeserver"]?["base_url"]); } catch (error) { return null; } } static final provider = AsyncNotifierProvider( ClientController.new, ); }