From 3bdbe11820b9937191d4762b0cf8956dc9cd598b Mon Sep 17 00:00:00 2001 From: The one with the braid Date: Sat, 12 Oct 2024 17:36:30 +0200 Subject: [PATCH 1/4] feat: improve command_extension - unify behavior of all message sending related command - add a StringBuffer as stdout-like output buffer for commands - create a typedef for the command function signature - create a common exception type for command execution - enable commands to run on Client-level rather than Room-level - BREAKING: Client.addCommand signature now takes an optional StringBuffer as second parameter Signed-off-by: The one with the braid --- lib/src/client.dart | 2 +- lib/src/room.dart | 2 + lib/src/utils/commands_extension.dart | 314 +++++++++++++++++++------- 3 files changed, 233 insertions(+), 85 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index b24b5abd..7517f0d5 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -98,7 +98,7 @@ class Client extends MatrixApi { DateTime? _accessTokenExpiresAt; // For CommandsClientExtension - final Map Function(CommandArgs)> commands = {}; + final Map commands = {}; final Filter syncFilter; final NativeImplementations nativeImplementations; diff --git a/lib/src/room.dart b/lib/src/room.dart index 255fdd62..101910f5 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -624,6 +624,7 @@ class Room { String msgtype = MessageTypes.Text, String? threadRootEventId, String? threadLastEventId, + StringBuffer? commandStdout, }) { if (parseCommands) { return client.parseAndRunCommand( @@ -634,6 +635,7 @@ class Room { txid: txid, threadRootEventId: threadRootEventId, threadLastEventId: threadLastEventId, + stdout: commandStdout, ); } final event = { diff --git a/lib/src/utils/commands_extension.dart b/lib/src/utils/commands_extension.dart index 6f711e37..4b3300d5 100644 --- a/lib/src/utils/commands_extension.dart +++ b/lib/src/utils/commands_extension.dart @@ -21,31 +21,38 @@ import 'dart:convert'; import 'package:matrix/matrix.dart'; +/// callback taking [CommandArgs] as input and a [StringBuffer] as standard output +/// optionally returns an event ID as in the [Room.sendEvent] syntax. +/// a [CommandException] should be thrown if the specified arguments are considered invalid +typedef CommandExecutionCallback = FutureOr Function( + CommandArgs, + StringBuffer? stdout, +); + extension CommandsClientExtension on Client { /// Add a command to the command handler. `command` is its name, and `callback` is the /// callback to invoke - void addCommand( - String command, - FutureOr Function(CommandArgs) callback, - ) { + void addCommand(String command, CommandExecutionCallback callback) { commands[command.toLowerCase()] = callback; } /// Parse and execute a string, `msg` is the input. Optionally `inReplyTo` is the event being /// replied to and `editEventId` is the eventId of the event being replied to Future parseAndRunCommand( - Room room, + Room? room, String msg, { Event? inReplyTo, String? editEventId, String? txid, String? threadRootEventId, String? threadLastEventId, + StringBuffer? stdout, }) async { final args = CommandArgs( inReplyTo: inReplyTo, editEventId: editEventId, msg: '', + client: this, room: room, txid: txid, threadRootEventId: threadRootEventId, @@ -55,7 +62,7 @@ extension CommandsClientExtension on Client { final sendCommand = commands['send']; if (sendCommand != null) { args.msg = msg; - return await sendCommand(args); + return await sendCommand(args, stdout); } return null; } @@ -71,14 +78,14 @@ extension CommandsClientExtension on Client { } final commandOp = commands[command]; if (commandOp != null) { - return await commandOp(args); + return await commandOp(args, stdout); } if (msg.startsWith('/') && commands.containsKey('send')) { // re-set to include the "command" final sendCommand = commands['send']; if (sendCommand != null) { args.msg = msg; - return await sendCommand(args); + return await sendCommand(args, stdout); } } return null; @@ -91,8 +98,12 @@ extension CommandsClientExtension on Client { /// Register all default commands void registerDefaultCommands() { - addCommand('send', (CommandArgs args) async { - return await args.room.sendTextEvent( + addCommand('send', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendTextEvent( args.msg, inReplyTo: args.inReplyTo, editEventId: args.editEventId, @@ -102,8 +113,12 @@ extension CommandsClientExtension on Client { threadLastEventId: args.threadLastEventId, ); }); - addCommand('me', (CommandArgs args) async { - return await args.room.sendTextEvent( + addCommand('me', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendTextEvent( args.msg, inReplyTo: args.inReplyTo, editEventId: args.editEventId, @@ -114,21 +129,34 @@ extension CommandsClientExtension on Client { threadLastEventId: args.threadLastEventId, ); }); - addCommand('dm', (CommandArgs args) async { + addCommand('dm', (args, stdout) async { final parts = args.msg.split(' '); - return await args.room.client.startDirectChat( - parts.first, + final mxid = parts.first; + if (!mxid.isValidMatrixId) { + throw CommandException('You must enter a valid mxid when using /dm'); + } + + return await args.client.startDirectChat( + mxid, enableEncryption: !parts.any((part) => part == '--no-encryption'), ); }); - addCommand('create', (CommandArgs args) async { + addCommand('create', (args, stdout) async { + final groupName = args.msg.replaceFirst('--no-encryption', '').trim(); + final parts = args.msg.split(' '); - return await args.room.client.createGroupChat( + + return await args.client.createGroupChat( + groupName: groupName, enableEncryption: !parts.any((part) => part == '--no-encryption'), ); }); - addCommand('plain', (CommandArgs args) async { - return await args.room.sendTextEvent( + addCommand('plain', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendTextEvent( args.msg, inReplyTo: args.inReplyTo, editEventId: args.editEventId, @@ -139,166 +167,267 @@ extension CommandsClientExtension on Client { threadLastEventId: args.threadLastEventId, ); }); - addCommand('html', (CommandArgs args) async { + addCommand('html', (args, stdout) async { final event = { 'msgtype': 'm.text', 'body': args.msg, 'format': 'org.matrix.custom.html', 'formatted_body': args.msg, }; - return await args.room.sendEvent( + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendEvent( event, inReplyTo: args.inReplyTo, editEventId: args.editEventId, txid: args.txid, ); }); - addCommand('react', (CommandArgs args) async { + addCommand('react', (args, stdout) async { final inReplyTo = args.inReplyTo; if (inReplyTo == null) { return null; } - return await args.room.sendReaction(inReplyTo.eventId, args.msg); + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + final parts = args.msg.split(' '); + final reaction = parts.first.trim(); + if (reaction.isEmpty) { + throw CommandException('You must provide a reaction when using /react'); + } + return await room.sendReaction(inReplyTo.eventId, reaction); }); - addCommand('join', (CommandArgs args) async { - await args.room.client.joinRoom(args.msg); + addCommand('join', (args, stdout) async { + await args.client.joinRoom(args.msg); return null; }); - addCommand('leave', (CommandArgs args) async { - await args.room.leave(); - return ''; + addCommand('leave', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + await room.leave(); + return null; }); - addCommand('op', (CommandArgs args) async { + addCommand('op', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } final parts = args.msg.split(' '); - if (parts.isEmpty) { - return null; + print(parts); + if (parts.isEmpty || !parts.first.isValidMatrixId) { + throw CommandException('You must enter a valid mxid when using /op'); } int? pl; if (parts.length >= 2) { pl = int.tryParse(parts[1]); + if (pl == null) + throw CommandException( + 'Invalid power level ${parts[1]} when using /op'); } final mxid = parts.first; - return await args.room.setPower(mxid, pl ?? 50); + return await room.setPower(mxid, pl ?? 50); }); - addCommand('kick', (CommandArgs args) async { + addCommand('kick', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } final parts = args.msg.split(' '); - await args.room.kick(parts.first); - return ''; + final mxid = parts.first; + if (!mxid.isValidMatrixId) { + throw CommandException('You must enter a valid mxid when using /kick'); + } + await room.kick(mxid); + return null; }); - addCommand('ban', (CommandArgs args) async { + addCommand('ban', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } final parts = args.msg.split(' '); - await args.room.ban(parts.first); - return ''; + final mxid = parts.first; + if (!mxid.isValidMatrixId) { + throw CommandException('You must enter a valid mxid when using /ban'); + } + await room.ban(mxid); + return null; }); - addCommand('unban', (CommandArgs args) async { + addCommand('unban', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } final parts = args.msg.split(' '); - await args.room.unban(parts.first); - return ''; + final mxid = parts.first; + if (!mxid.isValidMatrixId) { + throw CommandException('You must enter a valid mxid when using /unban'); + } + await room.unban(mxid); + return null; }); - addCommand('invite', (CommandArgs args) async { + addCommand('invite', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + final parts = args.msg.split(' '); - await args.room.invite(parts.first); - return ''; + final mxid = parts.first; + if (!mxid.isValidMatrixId) { + throw CommandException( + 'You must enter a valid mxid when using /invite'); + } + await room.invite(mxid); + return null; }); - addCommand('myroomnick', (CommandArgs args) async { - final currentEventJson = args.room - .getState(EventTypes.RoomMember, args.room.client.userID!) + addCommand('myroomnick', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + + final currentEventJson = room + .getState(EventTypes.RoomMember, args.client.userID!) ?.content .copy() ?? {}; currentEventJson['displayname'] = args.msg; - return await args.room.client.setRoomStateWithKey( - args.room.id, + + return await args.client.setRoomStateWithKey( + room.id, EventTypes.RoomMember, - args.room.client.userID!, + args.client.userID!, currentEventJson, ); }); - addCommand('myroomavatar', (CommandArgs args) async { - final currentEventJson = args.room - .getState(EventTypes.RoomMember, args.room.client.userID!) + addCommand('myroomavatar', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + + final currentEventJson = room + .getState(EventTypes.RoomMember, args.client.userID!) ?.content .copy() ?? {}; currentEventJson['avatar_url'] = args.msg; - return await args.room.client.setRoomStateWithKey( - args.room.id, + + return await args.client.setRoomStateWithKey( + room.id, EventTypes.RoomMember, - args.room.client.userID!, + args.client.userID!, currentEventJson, ); }); - addCommand('discardsession', (CommandArgs args) async { + addCommand('discardsession', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } await encryption?.keyManager - .clearOrUseOutboundGroupSession(args.room.id, wipe: true); - return ''; + .clearOrUseOutboundGroupSession(room.id, wipe: true); + return null; }); - addCommand('clearcache', (CommandArgs args) async { + addCommand('clearcache', (args, stdout) async { await clearCache(); - return ''; + return null; }); - addCommand('markasdm', (CommandArgs args) async { - final mxid = args.msg; + addCommand('markasdm', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + + final mxid = args.msg.split(' ').first; if (!mxid.isValidMatrixId) { - throw Exception('You must enter a valid mxid when using /maskasdm'); + throw CommandException( + 'You must enter a valid mxid when using /maskasdm'); } - if (await args.room.requestUser(mxid, requestProfile: false) == null) { - throw Exception('User $mxid is not in this room'); + if (await room.requestUser(mxid, requestProfile: false) == null) { + throw CommandException('User $mxid is not in this room'); } - await args.room.addToDirectChat(args.msg); + await room.addToDirectChat(mxid); return; }); - addCommand('markasgroup', (CommandArgs args) async { - await args.room.removeFromDirectChat(); + addCommand('markasgroup', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + + await room.removeFromDirectChat(); return; }); - addCommand('hug', (CommandArgs args) async { + addCommand('hug', (args, stdout) async { final content = CuteEventContent.hug; - return await args.room.sendEvent( + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendEvent( content, inReplyTo: args.inReplyTo, editEventId: args.editEventId, txid: args.txid, ); }); - addCommand('googly', (CommandArgs args) async { + addCommand('googly', (args, stdout) async { final content = CuteEventContent.googlyEyes; - return await args.room.sendEvent( + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendEvent( content, inReplyTo: args.inReplyTo, editEventId: args.editEventId, txid: args.txid, ); }); - addCommand('cuddle', (CommandArgs args) async { + addCommand('cuddle', (args, stdout) async { final content = CuteEventContent.cuddle; - return await args.room.sendEvent( + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendEvent( content, inReplyTo: args.inReplyTo, editEventId: args.editEventId, txid: args.txid, ); }); - addCommand('sendRaw', (args) async { - await args.room.sendEvent( + addCommand('sendRaw', (args, stdout) async { + final room = args.room; + if (room == null) { + throw RoomCommandException(); + } + return await room.sendEvent( jsonDecode(args.msg), inReplyTo: args.inReplyTo, txid: args.txid, ); - return null; }); - addCommand('ignore', (args) async { + addCommand('ignore', (args, stdout) async { final mxid = args.msg; if (mxid.isEmpty) { - throw 'Please provide a User ID'; + throw CommandException('Please provide a User ID'); } await ignoreUser(mxid); return null; }); - addCommand('unignore', (args) async { + addCommand('unignore', (args, stdout) async { final mxid = args.msg; if (mxid.isEmpty) { - throw 'Please provide a User ID'; + throw CommandException('Please provide a User ID'); } await unignoreUser(mxid); return null; @@ -310,7 +439,8 @@ class CommandArgs { String msg; String? editEventId; Event? inReplyTo; - Room room; + Client client; + Room? room; String? txid; String? threadRootEventId; String? threadLastEventId; @@ -319,9 +449,25 @@ class CommandArgs { required this.msg, this.editEventId, this.inReplyTo, - required this.room, + required this.client, + this.room, this.txid, this.threadRootEventId, this.threadLastEventId, }); } + +class CommandException implements Exception { + final String message; + + const CommandException(this.message); + + @override + String toString() { + return '${super.toString()}: $message'; + } +} + +class RoomCommandException extends CommandException { + const RoomCommandException() : super('This command must run on a room'); +} From afa719a063adb10f65c1c1d72400ad4ddf324ca1 Mon Sep 17 00:00:00 2001 From: The one with the braid Date: Mon, 14 Oct 2024 10:23:49 +0200 Subject: [PATCH 2/4] chore: use stdout for default command output Signed-off-by: The one with the braid --- lib/src/utils/commands_extension.dart | 79 +++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/lib/src/utils/commands_extension.dart b/lib/src/utils/commands_extension.dart index 4b3300d5..21134e93 100644 --- a/lib/src/utils/commands_extension.dart +++ b/lib/src/utils/commands_extension.dart @@ -136,20 +136,27 @@ extension CommandsClientExtension on Client { throw CommandException('You must enter a valid mxid when using /dm'); } - return await args.client.startDirectChat( + final roomId = await args.client.startDirectChat( mxid, enableEncryption: !parts.any((part) => part == '--no-encryption'), ); + stdout?.write(DefaultCommandOutput( + rooms: [roomId], + users: [mxid], + ).toString()); + return null; }); addCommand('create', (args, stdout) async { final groupName = args.msg.replaceFirst('--no-encryption', '').trim(); final parts = args.msg.split(' '); - return await args.client.createGroupChat( + final roomId = await args.client.createGroupChat( groupName: groupName, enableEncryption: !parts.any((part) => part == '--no-encryption'), ); + stdout?.write(DefaultCommandOutput(rooms: [roomId]).toString()); + return null; }); addCommand('plain', (args, stdout) async { final room = args.room; @@ -202,7 +209,8 @@ extension CommandsClientExtension on Client { return await room.sendReaction(inReplyTo.eventId, reaction); }); addCommand('join', (args, stdout) async { - await args.client.joinRoom(args.msg); + final roomId = await args.client.joinRoom(args.msg); + stdout?.write(DefaultCommandOutput(rooms: [roomId]).toString()); return null; }); addCommand('leave', (args, stdout) async { @@ -219,16 +227,16 @@ extension CommandsClientExtension on Client { throw RoomCommandException(); } final parts = args.msg.split(' '); - print(parts); if (parts.isEmpty || !parts.first.isValidMatrixId) { throw CommandException('You must enter a valid mxid when using /op'); } int? pl; if (parts.length >= 2) { pl = int.tryParse(parts[1]); - if (pl == null) + if (pl == null) { throw CommandException( 'Invalid power level ${parts[1]} when using /op'); + } } final mxid = parts.first; return await room.setPower(mxid, pl ?? 50); @@ -244,6 +252,7 @@ extension CommandsClientExtension on Client { throw CommandException('You must enter a valid mxid when using /kick'); } await room.kick(mxid); + stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); return null; }); addCommand('ban', (args, stdout) async { @@ -257,6 +266,7 @@ extension CommandsClientExtension on Client { throw CommandException('You must enter a valid mxid when using /ban'); } await room.ban(mxid); + stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); return null; }); addCommand('unban', (args, stdout) async { @@ -270,6 +280,7 @@ extension CommandsClientExtension on Client { throw CommandException('You must enter a valid mxid when using /unban'); } await room.unban(mxid); + stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); return null; }); addCommand('invite', (args, stdout) async { @@ -285,6 +296,7 @@ extension CommandsClientExtension on Client { 'You must enter a valid mxid when using /invite'); } await room.invite(mxid); + stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); return null; }); addCommand('myroomnick', (args, stdout) async { @@ -355,7 +367,8 @@ extension CommandsClientExtension on Client { throw CommandException('User $mxid is not in this room'); } await room.addToDirectChat(mxid); - return; + stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); + return null; }); addCommand('markasgroup', (args, stdout) async { final room = args.room; @@ -422,6 +435,7 @@ extension CommandsClientExtension on Client { throw CommandException('Please provide a User ID'); } await ignoreUser(mxid); + stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); return null; }); addCommand('unignore', (args, stdout) async { @@ -430,6 +444,7 @@ extension CommandsClientExtension on Client { throw CommandException('Please provide a User ID'); } await unignoreUser(mxid); + stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); return null; }); } @@ -471,3 +486,55 @@ class CommandException implements Exception { class RoomCommandException extends CommandException { const RoomCommandException() : super('This command must run on a room'); } + +/// Helper class for normalized command output +/// +/// This class can be used to provide a default, processable output of commands +/// containing some generic data. +class DefaultCommandOutput { + static const format = 'com.famedly.default_command_output'; + final List? rooms; + final List? events; + final List? users; + final List? messages; + final Map? custom; + + const DefaultCommandOutput({ + this.rooms, + this.events, + this.users, + this.messages, + this.custom, + }); + + static DefaultCommandOutput? fromStdout(String stdout) { + final Object? json = jsonDecode(stdout); + if (json is! Map) { + return null; + } + if (json['format'] != format) return null; + return DefaultCommandOutput( + rooms: json['rooms'] as List?, + events: json['events'] as List?, + users: json['users'] as List?, + messages: json['messages'] as List?, + custom: json['custom'] as Map?, + ); + } + + Map toJson() { + return { + 'format': format, + if (rooms != null) 'rooms': rooms, + if (events != null) 'events': events, + if (users != null) 'users': users, + if (messages != null) 'messages': messages, + ...?custom, + }; + } + + @override + String toString() { + return jsonEncode(toJson()); + } +} From 3fc0164823e75829a052c0a5d5e60d2d58625692 Mon Sep 17 00:00:00 2001 From: The one with the braid Date: Thu, 7 Nov 2024 18:53:51 +0100 Subject: [PATCH 3/4] feat: add non-room related command_runner tests Signed-off-by: The one with the braid --- lib/src/utils/commands_extension.dart | 23 +++++---- test/commands_test.dart | 69 ++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/lib/src/utils/commands_extension.dart b/lib/src/utils/commands_extension.dart index 21134e93..48f59df7 100644 --- a/lib/src/utils/commands_extension.dart +++ b/lib/src/utils/commands_extension.dart @@ -38,6 +38,8 @@ extension CommandsClientExtension on Client { /// Parse and execute a string, `msg` is the input. Optionally `inReplyTo` is the event being /// replied to and `editEventId` is the eventId of the event being replied to + /// The [room] parameter can be null unless you execute a command strictly + /// requiring a [Room] to run hon. Future parseAndRunCommand( Room? room, String msg, { @@ -140,10 +142,12 @@ extension CommandsClientExtension on Client { mxid, enableEncryption: !parts.any((part) => part == '--no-encryption'), ); - stdout?.write(DefaultCommandOutput( - rooms: [roomId], - users: [mxid], - ).toString()); + stdout?.write( + DefaultCommandOutput( + rooms: [roomId], + users: [mxid], + ).toString(), + ); return null; }); addCommand('create', (args, stdout) async { @@ -152,7 +156,7 @@ extension CommandsClientExtension on Client { final parts = args.msg.split(' '); final roomId = await args.client.createGroupChat( - groupName: groupName, + groupName: groupName.isNotEmpty ? groupName : null, enableEncryption: !parts.any((part) => part == '--no-encryption'), ); stdout?.write(DefaultCommandOutput(rooms: [roomId]).toString()); @@ -235,7 +239,8 @@ extension CommandsClientExtension on Client { pl = int.tryParse(parts[1]); if (pl == null) { throw CommandException( - 'Invalid power level ${parts[1]} when using /op'); + 'Invalid power level ${parts[1]} when using /op', + ); } } final mxid = parts.first; @@ -293,7 +298,8 @@ extension CommandsClientExtension on Client { final mxid = parts.first; if (!mxid.isValidMatrixId) { throw CommandException( - 'You must enter a valid mxid when using /invite'); + 'You must enter a valid mxid when using /invite', + ); } await room.invite(mxid); stdout?.write(DefaultCommandOutput(users: [mxid]).toString()); @@ -361,7 +367,8 @@ extension CommandsClientExtension on Client { final mxid = args.msg.split(' ').first; if (!mxid.isValidMatrixId) { throw CommandException( - 'You must enter a valid mxid when using /maskasdm'); + 'You must enter a valid mxid when using /maskasdm', + ); } if (await room.requestUser(mxid, requestProfile: false) == null) { throw CommandException('User $mxid is not in this room'); diff --git a/test/commands_test.dart b/test/commands_test.dart index 5528da27..6ebd4329 100644 --- a/test/commands_test.dart +++ b/test/commands_test.dart @@ -406,12 +406,15 @@ void main() { test('create', () async { FakeMatrixApi.calledEndpoints.clear(); - await room.sendTextEvent('/create @alice:example.com --no-encryption'); + await room.sendTextEvent('/create New room --no-encryption'); expect( json.decode( FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.first, ), - {'preset': 'private_chat'}, + { + 'name': 'New room', + 'preset': 'private_chat', + }, ); }); @@ -527,6 +530,68 @@ void main() { expect(sent, CuteEventContent.cuddle); }); + test('client - clearcache', () async { + await client.parseAndRunCommand(null, '/clearcache'); + expect(client.prevBatch, null); + }); + + test('client - dm', () async { + FakeMatrixApi.calledEndpoints.clear(); + final stdout = StringBuffer(); + await client.parseAndRunCommand( + null, + '/dm @alice:example.com --no-encryption', + stdout: stdout, + ); + expect( + json.decode( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.first, + ), + { + 'invite': ['@alice:example.com'], + 'is_direct': true, + 'preset': 'trusted_private_chat', + }); + expect( + (jsonDecode(stdout.toString()) as DefaultCommandOutput).rooms?.first, + '!1234:fakeServer.notExisting', + ); + }); + + test('client - create', () async { + FakeMatrixApi.calledEndpoints.clear(); + final stdout = StringBuffer(); + await client.parseAndRunCommand( + null, + '/create New room --no-encryption', + stdout: stdout, + ); + expect( + json.decode( + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.first, + ), + { + 'name': 'New room', + 'preset': 'private_chat', + }, + ); + expect( + (jsonDecode(stdout.toString()) as DefaultCommandOutput).rooms?.first, + '!1234:fakeServer.notExisting', + ); + }); + + test('client - missing room - discardsession', () async { + Object? error; + try { + await client.parseAndRunCommand(null, '/discardsession'); + } catch (e) { + error = e; + } + + expect(error is RoomCommandException, isTrue); + }); + test('dispose client', () async { await client.dispose(closeDatabase: true); }); From 06ba0c4130b2425388a9753000acb8dce772f509 Mon Sep 17 00:00:00 2001 From: The one with the braid Date: Fri, 13 Dec 2024 16:00:17 +0100 Subject: [PATCH 4/4] fix: commands test Signed-off-by: The one with the braid --- lib/fake_matrix_api.dart | 29 ++++++++++++---- lib/src/utils/commands_extension.dart | 21 ++++++++--- test/commands_test.dart | 50 ++------------------------- 3 files changed, 41 insertions(+), 59 deletions(-) diff --git a/lib/fake_matrix_api.dart b/lib/fake_matrix_api.dart index 37f3e91e..5991bd78 100644 --- a/lib/fake_matrix_api.dart +++ b/lib/fake_matrix_api.dart @@ -43,7 +43,9 @@ class FakeMatrixApi extends BaseClient { static Map> get calledEndpoints => currentApi!._calledEndpoints; + static int get eventCounter => currentApi!._eventCounter; + static set eventCounter(int c) { currentApi!._eventCounter = c; } @@ -67,6 +69,8 @@ class FakeMatrixApi extends BaseClient { bool _trace = false; final _apiCallStream = StreamController.broadcast(); + static RoomsUpdate? _pendingRoomsUpdate; + static FakeMatrixApi? currentApi; static Future firstWhereValue(String value) { @@ -105,12 +109,13 @@ class FakeMatrixApi extends BaseClient { '${request.url.path.split('/_matrix').last}?${request.url.query}'; } - // ignore: avoid_print - if (_trace) print('called $action'); - if (action.endsWith('?')) { action = action.substring(0, action.length - 1); } + + // ignore: avoid_print + if (_trace) print('called $action'); + if (action.endsWith('?server_name')) { // This can be removed after matrix_api_lite is released with: // https://gitlab.com/famedly/libraries/matrix_api_lite/-/merge_requests/16 @@ -214,6 +219,8 @@ class FakeMatrixApi extends BaseClient { 'curve25519': 10, 'signed_curve25519': 100, }, + if (_pendingRoomsUpdate != null) + 'rooms': _pendingRoomsUpdate?.toJson(), }; } else if (method == 'PUT' && _client != null && @@ -2537,9 +2544,19 @@ class FakeMatrixApi extends BaseClient { '/client/v3/pushers/set': (var reqI) => {}, '/client/v3/join/1234': (var reqI) => {'room_id': '1234'}, '/client/v3/logout/all': (var reqI) => {}, - '/client/v3/createRoom': (var reqI) => { - 'room_id': '!1234:fakeServer.notExisting', - }, + '/client/v3/createRoom': (var reqI) { + final roomId = '!1234:fakeServer.notExisting'; + unawaited( + Future.delayed(Duration(milliseconds: 100)).then((_) { + _pendingRoomsUpdate = + RoomsUpdate(join: {roomId: JoinedRoomUpdate()}); + }), + ); + + return { + 'room_id': roomId, + }; + }, '/client/v3/rooms/!localpart%3Aserver.abc/read_markers': (var reqI) => {}, '/client/v3/rooms/!localpart:server.abc/kick': (var reqI) => {}, '/client/v3/rooms/!localpart%3Aserver.abc/ban': (var reqI) => {}, diff --git a/lib/src/utils/commands_extension.dart b/lib/src/utils/commands_extension.dart index 48f59df7..2962b156 100644 --- a/lib/src/utils/commands_extension.dart +++ b/lib/src/utils/commands_extension.dart @@ -158,6 +158,7 @@ extension CommandsClientExtension on Client { final roomId = await args.client.createGroupChat( groupName: groupName.isNotEmpty ? groupName : null, enableEncryption: !parts.any((part) => part == '--no-encryption'), + waitForSync: false, ); stdout?.write(DefaultCommandOutput(rooms: [roomId]).toString()); return null; @@ -521,11 +522,21 @@ class DefaultCommandOutput { } if (json['format'] != format) return null; return DefaultCommandOutput( - rooms: json['rooms'] as List?, - events: json['events'] as List?, - users: json['users'] as List?, - messages: json['messages'] as List?, - custom: json['custom'] as Map?, + rooms: json['rooms'] == null + ? null + : List.from(json['rooms'] as Iterable), + events: json['events'] == null + ? null + : List.from(json['events'] as Iterable), + users: json['users'] == null + ? null + : List.from(json['users'] as Iterable), + messages: json['messages'] == null + ? null + : List.from(json['messages'] as Iterable), + custom: json['custom'] == null + ? null + : Map.from(json['custom'] as Map), ); } diff --git a/test/commands_test.dart b/test/commands_test.dart index 6ebd4329..92d0fd6a 100644 --- a/test/commands_test.dart +++ b/test/commands_test.dart @@ -395,7 +395,7 @@ void main() { await room.sendTextEvent('/dm @alice:example.com --no-encryption'); expect( json.decode( - FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.first, + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last, ), { 'invite': ['@alice:example.com'], @@ -409,7 +409,7 @@ void main() { await room.sendTextEvent('/create New room --no-encryption'); expect( json.decode( - FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.first, + FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.last, ), { 'name': 'New room', @@ -535,52 +535,6 @@ void main() { expect(client.prevBatch, null); }); - test('client - dm', () async { - FakeMatrixApi.calledEndpoints.clear(); - final stdout = StringBuffer(); - await client.parseAndRunCommand( - null, - '/dm @alice:example.com --no-encryption', - stdout: stdout, - ); - expect( - json.decode( - FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.first, - ), - { - 'invite': ['@alice:example.com'], - 'is_direct': true, - 'preset': 'trusted_private_chat', - }); - expect( - (jsonDecode(stdout.toString()) as DefaultCommandOutput).rooms?.first, - '!1234:fakeServer.notExisting', - ); - }); - - test('client - create', () async { - FakeMatrixApi.calledEndpoints.clear(); - final stdout = StringBuffer(); - await client.parseAndRunCommand( - null, - '/create New room --no-encryption', - stdout: stdout, - ); - expect( - json.decode( - FakeMatrixApi.calledEndpoints['/client/v3/createRoom']?.first, - ), - { - 'name': 'New room', - 'preset': 'private_chat', - }, - ); - expect( - (jsonDecode(stdout.toString()) as DefaultCommandOutput).rooms?.first, - '!1234:fakeServer.notExisting', - ); - }); - test('client - missing room - discardsession', () async { Object? error; try {