feat: Add command parser
This commit is contained in:
parent
5200764604
commit
fcb8d48bd7
|
|
@ -31,6 +31,7 @@ export 'src/utils/states_map.dart';
|
|||
export 'src/utils/sync_update_extension.dart';
|
||||
export 'src/utils/to_device_event.dart';
|
||||
export 'src/utils/uia_request.dart';
|
||||
export 'src/utils/commands_extension.dart';
|
||||
export 'src/client.dart';
|
||||
export 'src/event.dart';
|
||||
export 'src/room.dart';
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import 'database/database.dart' show Database;
|
|||
import 'event.dart';
|
||||
import 'room.dart';
|
||||
import 'user.dart';
|
||||
import 'utils/commands_extension.dart';
|
||||
import 'utils/device_keys_list.dart';
|
||||
import 'utils/event_update.dart';
|
||||
import 'utils/matrix_file.dart';
|
||||
|
|
@ -83,6 +84,9 @@ class Client extends MatrixApi {
|
|||
|
||||
bool mxidLocalPartFallback = true;
|
||||
|
||||
// For CommandsClientExtension
|
||||
final Map<String, FutureOr<String> Function(CommandArgs)> commands = {};
|
||||
|
||||
/// Create a client
|
||||
/// [clientName] = unique identifier of this client
|
||||
/// [database]: The database instance to use
|
||||
|
|
@ -147,6 +151,9 @@ class Client extends MatrixApi {
|
|||
EventTypes.Sticker,
|
||||
]);
|
||||
this.httpClient = httpClient ?? http.Client();
|
||||
|
||||
// register all the default commands
|
||||
registerDefaultCommands();
|
||||
}
|
||||
|
||||
/// The required name for this client.
|
||||
|
|
|
|||
|
|
@ -578,15 +578,17 @@ class Room {
|
|||
Event inReplyTo,
|
||||
String editEventId,
|
||||
bool parseMarkdown = true,
|
||||
Map<String, Map<String, String>> emotePacks}) {
|
||||
Map<String, Map<String, String>> emotePacks,
|
||||
bool parseCommands = true,
|
||||
String msgtype = MessageTypes.Text}) {
|
||||
if (parseCommands) {
|
||||
return client.parseAndRunCommand(this, message,
|
||||
inReplyTo: inReplyTo, editEventId: editEventId, txid: txid);
|
||||
}
|
||||
final event = <String, dynamic>{
|
||||
'msgtype': 'm.text',
|
||||
'msgtype': msgtype,
|
||||
'body': message,
|
||||
};
|
||||
if (message.startsWith('/me ')) {
|
||||
event['msgtype'] = 'm.emote';
|
||||
event['body'] = message.substring(4);
|
||||
}
|
||||
if (parseMarkdown) {
|
||||
final html = markdown(event['body'], emotePacks ?? this.emotePacks);
|
||||
// if the decoded html is the same as the body, there is no need in sending a formatted message
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../../famedlysdk.dart';
|
||||
|
||||
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<String> Function(CommandArgs) 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<String> parseAndRunCommand(Room room, String msg,
|
||||
{Event inReplyTo, String editEventId, String txid}) async {
|
||||
final args = CommandArgs(
|
||||
inReplyTo: inReplyTo,
|
||||
editEventId: editEventId,
|
||||
msg: '',
|
||||
room: room,
|
||||
txid: txid,
|
||||
);
|
||||
if (!msg.startsWith('/')) {
|
||||
if (commands.containsKey('send')) {
|
||||
args.msg = msg;
|
||||
return await commands['send'](args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// remove the /
|
||||
msg = msg.substring(1);
|
||||
var command = msg;
|
||||
if (msg.contains(' ')) {
|
||||
final idx = msg.indexOf(' ');
|
||||
command = msg.substring(0, idx).toLowerCase();
|
||||
args.msg = msg.substring(idx + 1);
|
||||
} else {
|
||||
command = msg.toLowerCase();
|
||||
}
|
||||
if (commands.containsKey(command)) {
|
||||
return await commands[command](args);
|
||||
}
|
||||
if (msg.startsWith('/') && commands.containsKey('send')) {
|
||||
// re-set to include the "command"
|
||||
args.msg = msg;
|
||||
return await commands['send'](args);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Unregister all commands
|
||||
void unregisterAllCommands() {
|
||||
commands.clear();
|
||||
}
|
||||
|
||||
/// Register all default commands
|
||||
void registerDefaultCommands() {
|
||||
addCommand('send', (CommandArgs args) async {
|
||||
return await args.room.sendTextEvent(
|
||||
args.msg,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
parseCommands: false,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('me', (CommandArgs args) async {
|
||||
return await args.room.sendTextEvent(
|
||||
args.msg,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
msgtype: MessageTypes.Emote,
|
||||
parseCommands: false,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('plain', (CommandArgs args) async {
|
||||
return await args.room.sendTextEvent(
|
||||
args.msg,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
parseMarkdown: false,
|
||||
parseCommands: false,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('html', (CommandArgs args) async {
|
||||
final event = <String, dynamic>{
|
||||
'msgtype': 'm.text',
|
||||
'body': args.msg,
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': args.msg,
|
||||
};
|
||||
return await args.room.sendEvent(
|
||||
event,
|
||||
inReplyTo: args.inReplyTo,
|
||||
editEventId: args.editEventId,
|
||||
txid: args.txid,
|
||||
);
|
||||
});
|
||||
addCommand('react', (CommandArgs args) async {
|
||||
if (args.inReplyTo == null) {
|
||||
return null;
|
||||
}
|
||||
return await args.room.sendReaction(args.inReplyTo.eventId, args.msg);
|
||||
});
|
||||
addCommand('join', (CommandArgs args) async {
|
||||
await args.room.client.joinRoomOrAlias(args.msg);
|
||||
return null;
|
||||
});
|
||||
addCommand('leave', (CommandArgs args) async {
|
||||
await args.room.leave();
|
||||
return '';
|
||||
});
|
||||
addCommand('op', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
if (parts.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
var pl = 50;
|
||||
if (parts.length >= 2) {
|
||||
pl = int.tryParse(parts[1]);
|
||||
}
|
||||
final mxid = parts.first;
|
||||
return await args.room.setPower(mxid, pl);
|
||||
});
|
||||
addCommand('kick', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.kick(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('ban', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.ban(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('unban', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.unban(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('invite', (CommandArgs args) async {
|
||||
final parts = args.msg.split(' ');
|
||||
await args.room.invite(parts.first);
|
||||
return '';
|
||||
});
|
||||
addCommand('myroomnick', (CommandArgs args) async {
|
||||
final currentEventJson = args.room
|
||||
.getState(EventTypes.RoomMember, args.room.client.userID)
|
||||
.content
|
||||
.copy();
|
||||
currentEventJson['displayname'] = args.msg;
|
||||
return await args.room.client.sendState(
|
||||
args.room.id,
|
||||
EventTypes.RoomMember,
|
||||
currentEventJson,
|
||||
args.room.client.userID,
|
||||
);
|
||||
});
|
||||
addCommand('myroomavatar', (CommandArgs args) async {
|
||||
final currentEventJson = args.room
|
||||
.getState(EventTypes.RoomMember, args.room.client.userID)
|
||||
.content
|
||||
.copy();
|
||||
currentEventJson['avatar_url'] = args.msg;
|
||||
return await args.room.client.sendState(
|
||||
args.room.id,
|
||||
EventTypes.RoomMember,
|
||||
currentEventJson,
|
||||
args.room.client.userID,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CommandArgs {
|
||||
String msg;
|
||||
String editEventId;
|
||||
Event inReplyTo;
|
||||
Room room;
|
||||
String txid;
|
||||
CommandArgs(
|
||||
{this.msg, this.editEventId, this.inReplyTo, this.room, this.txid});
|
||||
}
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* Famedly Matrix SDK
|
||||
* Copyright (C) 2021 Famedly GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:famedlysdk/famedlysdk.dart';
|
||||
import 'fake_client.dart';
|
||||
import 'fake_matrix_api.dart';
|
||||
|
||||
void main() {
|
||||
group('Commands', () {
|
||||
Client client;
|
||||
Room room;
|
||||
|
||||
final getLastMessagePayload =
|
||||
([String type = 'm.room.message', String stateKey]) {
|
||||
final state = stateKey != null;
|
||||
return json.decode(FakeMatrixApi.calledEndpoints.entries
|
||||
.firstWhere((e) => e.key.startsWith(
|
||||
'/client/r0/rooms/${Uri.encodeComponent(room.id)}/${state ? 'state' : 'send'}/${Uri.encodeComponent(type)}${state && stateKey.isNotEmpty ? '/' + Uri.encodeComponent(stateKey) : ''}'))
|
||||
.value
|
||||
.first);
|
||||
};
|
||||
|
||||
test('setupClient', () async {
|
||||
client = await getClient();
|
||||
room = Room(id: '!1234:fakeServer.notExisting', client: client);
|
||||
room.setState(Event(
|
||||
type: 'm.room.power_levels',
|
||||
content: {},
|
||||
room: room,
|
||||
stateKey: '',
|
||||
));
|
||||
room.setState(Event(
|
||||
type: 'm.room.member',
|
||||
content: {'membership': 'join'},
|
||||
room: room,
|
||||
stateKey: client.userID,
|
||||
));
|
||||
});
|
||||
|
||||
test('send', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/send Hello World');
|
||||
var sent = getLastMessagePayload();
|
||||
expect(sent, {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'Hello World',
|
||||
});
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('Beep Boop');
|
||||
sent = getLastMessagePayload();
|
||||
expect(sent, {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'Beep Boop',
|
||||
});
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('Beep *Boop*');
|
||||
sent = getLastMessagePayload();
|
||||
expect(sent, {
|
||||
'msgtype': 'm.text',
|
||||
'body': 'Beep *Boop*',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': 'Beep <em>Boop</em>',
|
||||
});
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('//send Hello World');
|
||||
sent = getLastMessagePayload();
|
||||
expect(sent, {
|
||||
'msgtype': 'm.text',
|
||||
'body': '/send Hello World',
|
||||
});
|
||||
});
|
||||
|
||||
test('me', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/me heya');
|
||||
var sent = getLastMessagePayload();
|
||||
expect(sent, {
|
||||
'msgtype': 'm.emote',
|
||||
'body': 'heya',
|
||||
});
|
||||
});
|
||||
|
||||
test('plain', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/plain *floof*');
|
||||
var sent = getLastMessagePayload();
|
||||
expect(sent, {
|
||||
'msgtype': 'm.text',
|
||||
'body': '*floof*',
|
||||
});
|
||||
});
|
||||
|
||||
test('html', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/html <b>yay</b>');
|
||||
var sent = getLastMessagePayload();
|
||||
expect(sent, {
|
||||
'msgtype': 'm.text',
|
||||
'body': '<b>yay</b>',
|
||||
'format': 'org.matrix.custom.html',
|
||||
'formatted_body': '<b>yay</b>',
|
||||
});
|
||||
});
|
||||
|
||||
test('react', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/react 🦊',
|
||||
inReplyTo: Event(eventId: '\$event'));
|
||||
var sent = getLastMessagePayload('m.reaction');
|
||||
expect(sent, {
|
||||
'm.relates_to': {
|
||||
'rel_type': 'm.annotation',
|
||||
'event_id': '\$event',
|
||||
'key': '🦊',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('join', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/join !newroom:example.com');
|
||||
expect(
|
||||
FakeMatrixApi
|
||||
.calledEndpoints['/client/r0/join/!newroom%3Aexample.com']
|
||||
.first !=
|
||||
null,
|
||||
true);
|
||||
});
|
||||
|
||||
test('leave', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/leave');
|
||||
expect(
|
||||
FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/leave']
|
||||
.first !=
|
||||
null,
|
||||
true);
|
||||
});
|
||||
|
||||
test('op', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/op @user:example.org');
|
||||
var sent = getLastMessagePayload('m.room.power_levels', '');
|
||||
expect(sent, {
|
||||
'users': {'@user:example.org': 50}
|
||||
});
|
||||
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/op @user:example.org 100');
|
||||
sent = getLastMessagePayload('m.room.power_levels', '');
|
||||
expect(sent, {
|
||||
'users': {'@user:example.org': 100}
|
||||
});
|
||||
});
|
||||
|
||||
test('kick', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/kick @baduser:example.org');
|
||||
expect(
|
||||
json.decode(FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/kick']
|
||||
.first),
|
||||
{
|
||||
'user_id': '@baduser:example.org',
|
||||
});
|
||||
});
|
||||
|
||||
test('ban', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/ban @baduser:example.org');
|
||||
expect(
|
||||
json.decode(FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/ban']
|
||||
.first),
|
||||
{
|
||||
'user_id': '@baduser:example.org',
|
||||
});
|
||||
});
|
||||
|
||||
test('unban', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/unban @baduser:example.org');
|
||||
expect(
|
||||
json.decode(FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/unban']
|
||||
.first),
|
||||
{
|
||||
'user_id': '@baduser:example.org',
|
||||
});
|
||||
});
|
||||
|
||||
test('invite', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/invite @baduser:example.org');
|
||||
expect(
|
||||
json.decode(FakeMatrixApi
|
||||
.calledEndpoints[
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/invite']
|
||||
.first),
|
||||
{
|
||||
'user_id': '@baduser:example.org',
|
||||
});
|
||||
});
|
||||
|
||||
test('myroomnick', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/myroomnick Foxies~');
|
||||
var sent = getLastMessagePayload('m.room.member', client.userID);
|
||||
expect(sent, {
|
||||
'displayname': 'Foxies~',
|
||||
'membership': 'join',
|
||||
});
|
||||
});
|
||||
|
||||
test('myroomavatar', () async {
|
||||
FakeMatrixApi.calledEndpoints.clear();
|
||||
await room.sendTextEvent('/myroomavatar mxc://beep/boop');
|
||||
var sent = getLastMessagePayload('m.room.member', client.userID);
|
||||
expect(sent, {
|
||||
'avatar_url': 'mxc://beep/boop',
|
||||
'membership': 'join',
|
||||
});
|
||||
});
|
||||
|
||||
test('dispose client', () async {
|
||||
await client.dispose(closeDatabase: true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -104,6 +104,10 @@ class FakeMatrixApi extends MockClient {
|
|||
action.contains(
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/send/')) {
|
||||
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
|
||||
} else if (method == 'PUT' &&
|
||||
action.contains(
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/state/')) {
|
||||
res = {'event_id': '\$event${FakeMatrixApi.eventCounter++}'};
|
||||
} else if (action.contains('/client/r0/sync')) {
|
||||
res = {
|
||||
'next_batch': DateTime.now().millisecondsSinceEpoch.toString
|
||||
|
|
@ -1830,16 +1834,23 @@ class FakeMatrixApi extends MockClient {
|
|||
}
|
||||
},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/invite': (var req) => {},
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/invite': (var req) => {},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/leave': (var req) => {},
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/leave': (var req) => {},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/forget': (var req) => {},
|
||||
'/client/r0/rooms/!localpart%3Aserver.abc/kick': (var req) => {},
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/kick': (var req) => {},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/kick': (var req) => {},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/ban': (var req) => {},
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/ban': (var req) => {},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/unban': (var req) => {},
|
||||
'/client/r0/rooms/!1234%3AfakeServer.notExisting/unban': (var req) => {},
|
||||
'/client/r0/rooms/!localpart%3Aexample.com/join': (var req) =>
|
||||
{'room_id': '!localpart:example.com'},
|
||||
'/client/r0/join/!localpart%3Aexample.com?server_name=example.com&server_name=example.abc':
|
||||
(var req) => {'room_id': '!localpart:example.com'},
|
||||
'/client/r0/join/!newroom%3Aexample.com': (var req) =>
|
||||
{'room_id': '!newroom%3A:example.com'},
|
||||
'/client/r0/keys/upload': (var req) => {
|
||||
'one_time_key_counts': {
|
||||
'curve25519': 10,
|
||||
|
|
|
|||
Loading…
Reference in New Issue