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/controllers/settings.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/mentions_response.dart"; import "package:nexus/models/gomuks_config.dart"; import "package:nexus/models/gomuks_exception.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/create_room.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_space_hierarchy.dart"; import "package:nexus/models/requests/join_room.dart"; import "package:nexus/models/profile_response.dart"; import "package:nexus/models/push_notification.dart"; import "package:nexus/models/recovery_key.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/search.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_summary.dart"; import "package:nexus/models/space_hierarchy.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); } }); if (!ref.mounted) { GomuksDestroy(handle); return Completer().future; } ref.onDispose(() => GomuksDestroy(handle)); return handle; } /// Calls gomuks with [data] as JSON, returning its decoded response. Future callGomuksMethod( Map data, void Function( int handle, GomuksOwnedBuffer data, Pointer> callback, ) call, ) async { final handle = await future; final buffer = data.toGomuksOwnedBuffer(); final completer = Completer(); late final NativeCallable callable; callable = .listener((GomuksResponse response) { callable.close(); try { final json = response.buf.toJsonAndFree(); if (response.command.cast().toDartString() == "error") { throw GomuksException(json.toString()); } completer.complete(json); } catch (error, stackTrace) { completer.completeError(error, stackTrace); } }); try { call(handle, buffer, callable.nativeFunction); } catch (_) { // gomuks never got the call, so it won't respond or free the buffer callable.close(); GomuksFreeBuffer(buffer); rethrow; } return completer.future; } Future handlePush(Map data) async { final response = await callGomuksMethod(data, GomuksHandlePushAsync); return response?["notification"] == null ? null : .fromJson(response["notification"]); } Future _sendCommand( String command, [ Map data = const {}, ]) => callGomuksMethod(data, (handle, data, callback) { final commandPointer = command.toNativeUtf8().cast(); try { GomuksSubmitCommandAsync(handle, commandPointer, data, callback); } finally { // gomuks copies the command name before returning 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 generateRecoveryKey(String passphrase) async => .fromJson( await _sendCommand("generate_recovery_key", {"passphrase": passphrase}), ); Future resetEncryption(RecoveryKey key) => _sendCommand("reset_encryption", key.toJson()); Future getCrossSigningResetUrl(Uri homeserver) async { const action = "org.matrix.cross_signing_reset"; final oauth = (await _sendCommand("get_login_flows", { "homeserver_url": homeserver.toString(), }))["oauth"]; if (oauth?["account_management_uri"] case final String uri when (oauth["account_management_actions_supported"] as List?)?.contains( action, ) ?? false) { final parsed = Uri.parse(uri); return parsed.replace( queryParameters: {...parsed.queryParameters, "action": action}, ); } return null; } Future joinRoom(JoinRoomRequest request) async => (await _sendCommand("join_room", request.toJson()))["room_id"]; Future knockRoom(JoinRoomRequest request) async => (await _sendCommand("knock_room", request.toJson()))["room_id"]; Future createRoom(CreateRoomRequest request) async => (await _sendCommand("create_room", request.toJson()))["room_id"]; Future getSpaceHierarchy( GetSpaceHierarchyRequest request, ) async => .fromJson(await _sendCommand("get_space_hierarchy", request.toJson())); Future getRoomSummary(JoinRoomRequest request) async => .fromJson(await _sendCommand("get_room_summary", request.toJson())); Future leaveRoom(String roomId) => _sendCommand("leave_room", {"room_id": roomId}); 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 => .fromJson(await _sendCommand("get_mentions", request.toJson())); 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 searchLocal(SearchRequest request) async => .fromJson(await _sendCommand("search_local", request.toJson())); Future searchServer(SearchRequest request) async => .fromJson(await _sendCommand("search_server", 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 setProfileField(String field, Object? value) => _sendCommand( "set_profile_field", { "field": field, // gomuks deletes the field when the value is omitted, not when it's null "value": ?value, }, ); 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 setTyping(String roomId, Duration timeout) => _sendCommand( "set_typing", {"room_id": roomId, "timeout": timeout.inMilliseconds}, ); 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; // Private until settings have loaded, in case the user turned sending off final sendPublic = ref.read(SettingsController.provider).value?.readReceipts == .showAndSend; await _sendCommand("mark_read", { "room_id": room.metadata!.id, "receipt_type": sendPublic ? "m.read" : "m.read.private", "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> getMutualRooms(String userId) async => IList( ((await _sendCommand("get_mutual_rooms", {"user_id": userId}))["joined"] as List?) ?.cast(), ); Future> getLocalAliases(String roomId) async => IList( ((await _sendCommand("get_local_aliases", {"room_id": roomId}))["aliases"] as List?) ?.cast(), ); Future createAlias(String roomId, String alias) => _sendCommand("create_alias", {"room_id": roomId, "alias": alias}); Future deleteAlias(String alias) => _sendCommand("delete_alias", {"alias": alias}); /// The room [alias] points to, or null if it doesn't exist. Future resolveAlias(String alias) async { try { return (await _sendCommand("resolve_alias", {"alias": alias}))["room_id"]; } on GomuksException catch (error) { if (error.statusCode == 404) return null; rethrow; } } 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, ); }