Gomuks SDK Rewrite #2

Closed
Henry-Hiles wants to merge 34 commits from go into main
21 changed files with 236 additions and 121 deletions
Showing only changes of commit 92eedc92ab - Show all commits

wip thing

Henry Hiles 2026-01-29 11:52:56 +00:00
No known key found for this signature in database

View file

@ -12,12 +12,15 @@ import "package:nexus/controllers/top_level_spaces_controller.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/models/client_state.dart"; import "package:nexus/models/client_state.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/get_event_request.dart"; import "package:nexus/models/paginate.dart";
import "package:nexus/models/get_related_events_request.dart"; import "package:nexus/models/requests/get_event_request.dart";
import "package:nexus/models/login_request.dart"; import "package:nexus/models/requests/get_related_events_request.dart";
import "package:nexus/models/requests/login_request.dart";
import "package:nexus/models/profile.dart"; import "package:nexus/models/profile.dart";
import "package:nexus/models/redact_event_request.dart"; import "package:nexus/models/requests/paginate_request.dart";
import "package:nexus/models/report_request.dart"; import "package:nexus/models/requests/redact_event_request.dart";
import "package:nexus/models/requests/report_request.dart";
import "package:nexus/models/requests/send_message_request.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/models/sync_data.dart"; import "package:nexus/models/sync_data.dart";
import "package:nexus/models/sync_status.dart"; import "package:nexus/models/sync_status.dart";
@ -98,7 +101,10 @@ class ClientController extends AsyncNotifier<int> {
throw Exception("GomuksStart returned error code $errorCode"); throw Exception("GomuksStart returned error code $errorCode");
} }
Future<dynamic> sendCommand(String command, Map<String, dynamic> data) async { Future<dynamic> _sendCommand(
String command,
Map<String, dynamic> data,
) async {
final bufferPointer = data.toGomuksBufferPtr(); final bufferPointer = data.toGomuksBufferPtr();
final handle = await future; final handle = await future;
final response = await Isolate.run( final response = await Isolate.run(
@ -115,11 +121,14 @@ class ClientController extends AsyncNotifier<int> {
} }
Future<void> redactEvent(RedactEventRequest report) => Future<void> redactEvent(RedactEventRequest report) =>
sendCommand("redact_event", report.toJson()); _sendCommand("redact_event", report.toJson());
Future<void> sendMessage(SendMessageRequest request) =>
_sendCommand("send_message", request.toJson());
Future<bool> verify(String recoveryKey) async { Future<bool> verify(String recoveryKey) async {
try { try {
await sendCommand("verify", {"recovery_key": recoveryKey}); await _sendCommand("verify", {"recovery_key": recoveryKey});
return true; return true;
} catch (error) { } catch (error) {
return false; return false;
@ -128,35 +137,38 @@ class ClientController extends AsyncNotifier<int> {
Future<void> leaveRoom(Room room) async { Future<void> leaveRoom(Room room) async {
if (room.metadata == null) return; if (room.metadata == null) return;
await sendCommand("leave_room", {"room_id": room.metadata!.id}); await _sendCommand("leave_room", {"room_id": room.metadata!.id});
} }
Future<IList<Event>?> getRelatedEvents( Future<IList<Event>?> getRelatedEvents(
GetRelatedEventsRequest request, GetRelatedEventsRequest request,
) async { ) async {
final response = final response =
(await sendCommand("get_related_events", request.toJson())) as List?; (await _sendCommand("get_related_events", request.toJson())) as List?;
return response?.map((event) => Event.fromJson(event)).toIList(); return response?.map((event) => Event.fromJson(event)).toIList();
} }
Future<Event?> getEvent(GetEventRequest request) async { Future<Event?> getEvent(GetEventRequest request) async {
final json = await sendCommand("get_event", request.toJson()); final json = await _sendCommand("get_event", request.toJson());
return json == null ? null : Event.fromJson(json); return json == null ? null : Event.fromJson(json);
} }
Future<Paginate> paginate(PaginateRequest request) async =>
Paginate.fromJson(await _sendCommand("paginate", request.toJson()));
Future<Profile?> getProfile(String userId) async { Future<Profile?> getProfile(String userId) async {
final json = await sendCommand("get_profile", {"user_id": userId}); final json = await _sendCommand("get_profile", {"user_id": userId});
return json == null ? null : Profile.fromJson(json); return json == null ? null : Profile.fromJson(json);
} }
Future<void> reportEvent(ReportRequest report) => Future<void> reportEvent(ReportRequest report) =>
sendCommand("report_event", report.toJson()); _sendCommand("report_event", report.toJson());
Future<void> markRead(Room room) async { Future<void> markRead(Room room) async {
if (room.events.isEmpty || room.metadata == null) return; if (room.events.isEmpty || room.metadata == null) return;
await sendCommand("mark_read", { await _sendCommand("mark_read", {
"room_id": room.metadata?.id, "room_id": room.metadata?.id,
"receipt_type": "m.read", "receipt_type": "m.read",
"event_id": room.events.last.eventId, "event_id": room.events.last.eventId,
@ -165,7 +177,7 @@ class ClientController extends AsyncNotifier<int> {
Future<bool> login(LoginRequest login) async { Future<bool> login(LoginRequest login) async {
try { try {
await sendCommand("login", login.toJson()); await _sendCommand("login", login.toJson());
return true; return true;
} catch (error) { } catch (error) {
return false; return false;
@ -174,7 +186,7 @@ class ClientController extends AsyncNotifier<int> {
Future<String?> discoverHomeserver(Uri homeserver) async { Future<String?> discoverHomeserver(Uri homeserver) async {
try { try {
final response = await sendCommand("discover_homeserver", { final response = await _sendCommand("discover_homeserver", {
"user_id": "@fakeuser:${homeserver.host}", "user_id": "@fakeuser:${homeserver.host}",
}); });
return response["m.homeserver"]?["base_url"]; return response["m.homeserver"]?["base_url"];

View file

@ -1,3 +1,5 @@
import "dart:ffi";
import "dart:isolate";
import "package:ffi/ffi.dart"; import "package:ffi/ffi.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";
@ -7,13 +9,14 @@ class HeaderController extends AsyncNotifier<Map<String, String>> {
@override @override
Future<Map<String, String>> build() async { Future<Map<String, String>> build() async {
final handle = await ref.watch(ClientController.provider.future); final handle = await ref.watch(ClientController.provider.future);
final info = GomuksGetAccountInfo(handle); final info = await Isolate.run(() => GomuksGetAccountInfo(handle));
final headers = { final headers = {
if (info.access_token != nullptr)
"authorization": "authorization":
"Bearer ${info.access_token.cast<Utf8>().toDartString()}", "Bearer ${info.access_token.cast<Utf8>().toDartString()}",
}; };
GomuksFreeAccountInfo(info); await Isolate.run(() => GomuksFreeAccountInfo(info));
return headers; return headers;
} }

View file

@ -9,8 +9,11 @@ import "package:nexus/controllers/new_events_controller.dart";
import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart";
import "package:nexus/helpers/extensions/event_to_message.dart"; import "package:nexus/helpers/extensions/event_to_message.dart";
import "package:nexus/helpers/extensions/list_to_messages.dart"; import "package:nexus/helpers/extensions/list_to_messages.dart";
import "package:nexus/models/redact_event_request.dart"; import "package:nexus/models/requests/redact_event_request.dart";
import "package:nexus/models/relation_type.dart"; import "package:nexus/models/relation_type.dart";
import "package:nexus/models/requests/send_message_request.dart";
import "package:nexus/models/room.dart";
import "package:nexus/models/sync_data.dart";
class RoomChatController extends AsyncNotifier<ChatController> { class RoomChatController extends AsyncNotifier<ChatController> {
final String roomId; final String roomId;
@ -67,7 +70,11 @@ class RoomChatController extends AsyncNotifier<ChatController> {
); );
final messages = await events.toMessages(client); final messages = await events.toMessages(client);
return InMemoryChatController(messages: messages); final controller = InMemoryChatController(messages: messages);
if (messages.length < 20) await loadOlder(controller);
return controller;
} }
Future<void> insertMessage(Message message) async { Future<void> insertMessage(Message message) async {
@ -98,9 +105,12 @@ class RoomChatController extends AsyncNotifier<ChatController> {
); );
} }
Future<void> loadOlder() async { Future<void> loadOlder([InMemoryChatController? chatController]) async {
// final currentEvents = await future; final controller = chatController ?? await future;
// await ref.watch(EventsController.provider(room).notifier).prev(); final client = ref.watch(ClientController.provider.notifier);
client.
// await ref.watchInMemoryChatController? chatController(EventsController.provider(room).notifier).prev();
// final timeline = await ref.watch(EventsController.provider(room).future); // final timeline = await ref.watch(EventsController.provider(room).future);
// final controller = await future; // final controller = await future;
@ -127,25 +137,28 @@ class RoomChatController extends AsyncNotifier<ChatController> {
required RelationType relationType, required RelationType relationType,
Message? relation, Message? relation,
}) async { }) async {
// var taggedMessage = message; var taggedMessage = message;
// for (final tag in tags) { for (final tag in tags) {
// final escaped = RegExp.escape(tag.id); final escaped = RegExp.escape(tag.id); // TODO: Fix
// final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?"); final pattern = RegExp(r"@+(" + escaped + r")(#[^#]*#)?");
// taggedMessage = taggedMessage.replaceAllMapped( taggedMessage = taggedMessage.replaceAllMapped(
// pattern, pattern,
// (match) => match.group(1)!, (match) => match.group(1)!,
// ); );
// } }
// await room.sendTextEvent( final client = ref.watch(ClientController.provider.notifier);
// taggedMessage, client.sendMessage(
// editEventId: relationType == RelationType.edit ? relation?.id : null, SendMessageRequest(
// inReplyTo: (relationType == RelationType.reply && relation != null) roomId: roomId,
// ? await room.getEventById(relation.id) text: taggedMessage,
// : null, relation: relation == null
// ); ? null
: Relation(eventId: relation.id, relationType: relationType),
),
);
} }
Future<chat.User> resolveUser(String id) async { Future<chat.User> resolveUser(String id) async {

View file

@ -45,19 +45,21 @@ class SpacesController extends Notifier<IList<Space>> {
) )
.toISet(); .toISet();
final dmRooms = rooms.values final otherRooms = rooms.entries
.where((room) => room.metadata?.dmUserId != null)
.toIList();
final homeRooms = rooms.entries
.where( .where(
(e) => (e) =>
e.value.metadata?.dmUserId == null &&
!allNestedRoomIds.contains(e.key) && !allNestedRoomIds.contains(e.key) &&
!topLevelSpaceIds.contains(e.key) && !topLevelSpaceIds.contains(e.key) &&
!spaceEdges.containsKey(e.key), !spaceEdges.containsKey(e.key),
) )
.map((e) => e.value) .map((e) => e.value);
final homeRooms = otherRooms
.where((room) => room.metadata?.dmUserId == null)
.toIList();
final dmRooms = otherRooms
.where((room) => room.metadata?.dmUserId != null)
.toIList(); .toIList();
final topLevelSpacesList = topLevelSpaceIds final topLevelSpacesList = topLevelSpaceIds

View file

@ -1,8 +1,8 @@
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/get_event_request.dart"; import "package:nexus/models/requests/get_event_request.dart";
import "package:nexus/models/get_related_events_request.dart"; import "package:nexus/models/requests/get_related_events_request.dart";
extension EventToMessage on Event { extension EventToMessage on Event {
Future<Message?> toMessage( Future<Message?> toMessage(
@ -92,8 +92,8 @@ extension EventToMessage on Event {
// ), // ),
("m.sticker" || "m.room.message") => switch (content["msgtype"]) { ("m.sticker" || "m.room.message") => switch (content["msgtype"]) {
("m.sticker" || "m.image") => Message.image( ("m.sticker" || "m.image") => Message.image(
metadata: metadata,
id: eventId, id: eventId,
metadata: metadata,
authorId: authorId, authorId: authorId,
text: event.localContent?.sanitizedHtml, text: event.localContent?.sanitizedHtml,
source: "(await getAttachmentUri()).toString()", // TODO source: "(await getAttachmentUri()).toString()", // TODO
@ -102,8 +102,8 @@ extension EventToMessage on Event {
blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"], blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"],
), ),
"m.audio" => Message.audio( "m.audio" => Message.audio(
metadata: metadata,
id: eventId, id: eventId,
metadata: metadata,
authorId: authorId, authorId: authorId,
text: content["body"], text: content["body"],
replyToMessageId: replyId, replyToMessageId: replyId,

View file

@ -8,7 +8,7 @@ abstract class ClientState with _$ClientState {
required bool isInitialized, required bool isInitialized,
required bool isLoggedIn, required bool isLoggedIn,
required bool isVerified, required bool isVerified,
required String userId, required String? userId,
}) = _ClientState; }) = _ClientState;
factory ClientState.fromJson(Map<String, Object?> json) => factory ClientState.fromJson(Map<String, Object?> json) =>

16
lib/models/paginate.dart Normal file
View file

@ -0,0 +1,16 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/event.dart";
part "paginate.freezed.dart";
part "paginate.g.dart";
@freezed
abstract class Paginate with _$Paginate {
const factory Paginate({
required IList<Event> events,
required bool hasMore,
}) = _Paginate;
factory Paginate.fromJson(Map<String, Object?> json) =>
_$PaginateFromJson(json);
}

View file

@ -1,3 +0,0 @@
import "package:nexus/models/report_request.dart";
typedef RedactEventRequest = ReportRequest;

View file

@ -0,0 +1,14 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "paginate_request.freezed.dart";
part "paginate_request.g.dart";
@freezed
abstract class PaginateRequest with _$PaginateRequest {
const factory PaginateRequest({
required String roomId,
@Default(20) int limit,
}) = _PaginateRequest;
factory PaginateRequest.fromJson(Map<String, Object?> json) =>
_$PaginateRequestFromJson(json);
}

View file

@ -0,0 +1,3 @@
import "package:nexus/models/requests/report_request.dart";
typedef RedactEventRequest = ReportRequest;

View file

@ -0,0 +1,55 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/relation_type.dart";
part "send_message_request.freezed.dart";
part "send_message_request.g.dart";
@freezed
abstract class SendMessageRequest with _$SendMessageRequest {
const factory SendMessageRequest({
required String roomId,
required String text,
@Default(Mentions()) @JsonKey(name: "m.mentions") Mentions mentions,
@JsonKey(name: "m.relates_to") Relation? relation,
}) = _SendMessageRequest;
factory SendMessageRequest.fromJson(Map<String, Object?> json) =>
_$SendMessageRequestFromJson(json);
}
@freezed
abstract class Mentions with _$Mentions {
const factory Mentions({
@Default(false) bool room,
@Default(IList.empty()) IList<String> userIds,
}) = _Mentions;
factory Mentions.fromJson(Map<String, Object?> json) =>
_$MentionsFromJson(json);
}
@freezed
abstract class Relation with _$Relation {
const Relation._(); // required for custom methods
const factory Relation({
required String eventId,
required RelationType relationType,
}) = _Relation;
@override
Map<String, Object?> toJson() {
switch (relationType) {
case RelationType.reply:
return {
"m.in_reply_to": {"event_id": eventId},
};
case RelationType.edit:
return {"rel_type": "m.replace", "event_id": eventId};
}
}
factory Relation.fromJson(Map<String, Object?> json) =>
_$RelationFromJson(json);
}

View file

@ -5,7 +5,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/models/homeserver.dart"; import "package:nexus/models/homeserver.dart";
import "package:nexus/models/login_request.dart"; import "package:nexus/models/requests/login_request.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/divider_text.dart";
import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/loading.dart";

View file

@ -18,7 +18,7 @@ import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/get_headers.dart";
import "package:nexus/helpers/extensions/show_context_menu.dart"; import "package:nexus/helpers/extensions/show_context_menu.dart";
import "package:nexus/models/relation_type.dart"; import "package:nexus/models/relation_type.dart";
import "package:nexus/models/report_request.dart"; import "package:nexus/models/requests/report_request.dart";
import "package:nexus/widgets/chat_page/chat_box.dart"; import "package:nexus/widgets/chat_page/chat_box.dart";
import "package:nexus/widgets/chat_page/html/html.dart"; import "package:nexus/widgets/chat_page/html/html.dart";
import "package:nexus/widgets/chat_page/room_appbar.dart"; import "package:nexus/widgets/chat_page/room_appbar.dart";