Line data Source code
1 : /*
2 : * Famedly Matrix SDK
3 : * Copyright (C) 2021 Famedly GmbH
4 : *
5 : * This program is free software: you can redistribute it and/or modify
6 : * it under the terms of the GNU Affero General Public License as
7 : * published by the Free Software Foundation, either version 3 of the
8 : * License, or (at your option) any later version.
9 : *
10 : * This program is distributed in the hope that it will be useful,
11 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 : * GNU Affero General Public License for more details.
14 : *
15 : * You should have received a copy of the GNU Affero General Public License
16 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 : */
18 :
19 : import 'dart:async';
20 : import 'dart:convert';
21 :
22 : import 'package:matrix/matrix.dart';
23 :
24 : extension CommandsClientExtension on Client {
25 : /// Add a command to the command handler. `command` is its name, and `callback` is the
26 : /// callback to invoke
27 39 : void addCommand(
28 : String command,
29 : FutureOr<String?> Function(CommandArgs) callback,
30 : ) {
31 117 : commands[command.toLowerCase()] = callback;
32 : }
33 :
34 : /// Parse and execute a string, `msg` is the input. Optionally `inReplyTo` is the event being
35 : /// replied to and `editEventId` is the eventId of the event being replied to
36 5 : Future<String?> parseAndRunCommand(
37 : Room room,
38 : String msg, {
39 : Event? inReplyTo,
40 : String? editEventId,
41 : String? txid,
42 : String? threadRootEventId,
43 : String? threadLastEventId,
44 : }) async {
45 5 : final args = CommandArgs(
46 : inReplyTo: inReplyTo,
47 : editEventId: editEventId,
48 : msg: '',
49 : room: room,
50 : txid: txid,
51 : threadRootEventId: threadRootEventId,
52 : threadLastEventId: threadLastEventId,
53 : );
54 5 : if (!msg.startsWith('/')) {
55 10 : final sendCommand = commands['send'];
56 : if (sendCommand != null) {
57 5 : args.msg = msg;
58 5 : return await sendCommand(args);
59 : }
60 : return null;
61 : }
62 : // remove the /
63 1 : msg = msg.substring(1);
64 : var command = msg;
65 1 : if (msg.contains(' ')) {
66 1 : final idx = msg.indexOf(' ');
67 2 : command = msg.substring(0, idx).toLowerCase();
68 3 : args.msg = msg.substring(idx + 1);
69 : } else {
70 1 : command = msg.toLowerCase();
71 : }
72 2 : final commandOp = commands[command];
73 : if (commandOp != null) {
74 1 : return await commandOp(args);
75 : }
76 3 : if (msg.startsWith('/') && commands.containsKey('send')) {
77 : // re-set to include the "command"
78 2 : final sendCommand = commands['send'];
79 : if (sendCommand != null) {
80 1 : args.msg = msg;
81 1 : return await sendCommand(args);
82 : }
83 : }
84 : return null;
85 : }
86 :
87 : /// Unregister all commands
88 0 : void unregisterAllCommands() {
89 0 : commands.clear();
90 : }
91 :
92 : /// Register all default commands
93 39 : void registerDefaultCommands() {
94 44 : addCommand('send', (CommandArgs args) async {
95 10 : return await args.room.sendTextEvent(
96 5 : args.msg,
97 5 : inReplyTo: args.inReplyTo,
98 5 : editEventId: args.editEventId,
99 : parseCommands: false,
100 5 : txid: args.txid,
101 5 : threadRootEventId: args.threadRootEventId,
102 5 : threadLastEventId: args.threadLastEventId,
103 : );
104 : });
105 40 : addCommand('me', (CommandArgs args) async {
106 2 : return await args.room.sendTextEvent(
107 1 : args.msg,
108 1 : inReplyTo: args.inReplyTo,
109 1 : editEventId: args.editEventId,
110 : msgtype: MessageTypes.Emote,
111 : parseCommands: false,
112 1 : txid: args.txid,
113 1 : threadRootEventId: args.threadRootEventId,
114 1 : threadLastEventId: args.threadLastEventId,
115 : );
116 : });
117 40 : addCommand('dm', (CommandArgs args) async {
118 2 : final parts = args.msg.split(' ');
119 3 : return await args.room.client.startDirectChat(
120 1 : parts.first,
121 3 : enableEncryption: !parts.any((part) => part == '--no-encryption'),
122 : );
123 : });
124 40 : addCommand('create', (CommandArgs args) async {
125 2 : final parts = args.msg.split(' ');
126 3 : return await args.room.client.createGroupChat(
127 3 : enableEncryption: !parts.any((part) => part == '--no-encryption'),
128 : );
129 : });
130 40 : addCommand('plain', (CommandArgs args) async {
131 2 : return await args.room.sendTextEvent(
132 1 : args.msg,
133 1 : inReplyTo: args.inReplyTo,
134 1 : editEventId: args.editEventId,
135 : parseMarkdown: false,
136 : parseCommands: false,
137 1 : txid: args.txid,
138 1 : threadRootEventId: args.threadRootEventId,
139 1 : threadLastEventId: args.threadLastEventId,
140 : );
141 : });
142 40 : addCommand('html', (CommandArgs args) async {
143 1 : final event = <String, dynamic>{
144 : 'msgtype': 'm.text',
145 1 : 'body': args.msg,
146 : 'format': 'org.matrix.custom.html',
147 1 : 'formatted_body': args.msg,
148 : };
149 2 : return await args.room.sendEvent(
150 : event,
151 1 : inReplyTo: args.inReplyTo,
152 1 : editEventId: args.editEventId,
153 1 : txid: args.txid,
154 : );
155 : });
156 40 : addCommand('react', (CommandArgs args) async {
157 1 : final inReplyTo = args.inReplyTo;
158 : if (inReplyTo == null) {
159 : return null;
160 : }
161 4 : return await args.room.sendReaction(inReplyTo.eventId, args.msg);
162 : });
163 40 : addCommand('join', (CommandArgs args) async {
164 4 : await args.room.client.joinRoom(args.msg);
165 : return null;
166 : });
167 40 : addCommand('leave', (CommandArgs args) async {
168 2 : await args.room.leave();
169 : return '';
170 : });
171 40 : addCommand('op', (CommandArgs args) async {
172 2 : final parts = args.msg.split(' ');
173 1 : if (parts.isEmpty) {
174 : return null;
175 : }
176 : int? pl;
177 2 : if (parts.length >= 2) {
178 2 : pl = int.tryParse(parts[1]);
179 : }
180 1 : final mxid = parts.first;
181 2 : return await args.room.setPower(mxid, pl ?? 50);
182 : });
183 40 : addCommand('kick', (CommandArgs args) async {
184 2 : final parts = args.msg.split(' ');
185 3 : await args.room.kick(parts.first);
186 : return '';
187 : });
188 40 : addCommand('ban', (CommandArgs args) async {
189 2 : final parts = args.msg.split(' ');
190 3 : await args.room.ban(parts.first);
191 : return '';
192 : });
193 40 : addCommand('unban', (CommandArgs args) async {
194 2 : final parts = args.msg.split(' ');
195 3 : await args.room.unban(parts.first);
196 : return '';
197 : });
198 40 : addCommand('invite', (CommandArgs args) async {
199 2 : final parts = args.msg.split(' ');
200 3 : await args.room.invite(parts.first);
201 : return '';
202 : });
203 40 : addCommand('myroomnick', (CommandArgs args) async {
204 1 : final currentEventJson = args.room
205 4 : .getState(EventTypes.RoomMember, args.room.client.userID!)
206 1 : ?.content
207 1 : .copy() ??
208 0 : {};
209 2 : currentEventJson['displayname'] = args.msg;
210 3 : return await args.room.client.setRoomStateWithKey(
211 2 : args.room.id,
212 : EventTypes.RoomMember,
213 3 : args.room.client.userID!,
214 : currentEventJson,
215 : );
216 : });
217 40 : addCommand('myroomavatar', (CommandArgs args) async {
218 1 : final currentEventJson = args.room
219 4 : .getState(EventTypes.RoomMember, args.room.client.userID!)
220 1 : ?.content
221 1 : .copy() ??
222 0 : {};
223 2 : currentEventJson['avatar_url'] = args.msg;
224 3 : return await args.room.client.setRoomStateWithKey(
225 2 : args.room.id,
226 : EventTypes.RoomMember,
227 3 : args.room.client.userID!,
228 : currentEventJson,
229 : );
230 : });
231 40 : addCommand('discardsession', (CommandArgs args) async {
232 2 : await encryption?.keyManager
233 3 : .clearOrUseOutboundGroupSession(args.room.id, wipe: true);
234 : return '';
235 : });
236 40 : addCommand('clearcache', (CommandArgs args) async {
237 1 : await clearCache();
238 : return '';
239 : });
240 40 : addCommand('markasdm', (CommandArgs args) async {
241 1 : final mxid = args.msg;
242 1 : if (!mxid.isValidMatrixId) {
243 0 : throw Exception('You must enter a valid mxid when using /maskasdm');
244 : }
245 2 : if (await args.room.requestUser(mxid, requestProfile: false) == null) {
246 0 : throw Exception('User $mxid is not in this room');
247 : }
248 3 : await args.room.addToDirectChat(args.msg);
249 : return;
250 : });
251 40 : addCommand('markasgroup', (CommandArgs args) async {
252 2 : await args.room.removeFromDirectChat();
253 : return;
254 : });
255 40 : addCommand('hug', (CommandArgs args) async {
256 1 : final content = CuteEventContent.hug;
257 2 : return await args.room.sendEvent(
258 : content,
259 1 : inReplyTo: args.inReplyTo,
260 1 : editEventId: args.editEventId,
261 1 : txid: args.txid,
262 : );
263 : });
264 40 : addCommand('googly', (CommandArgs args) async {
265 1 : final content = CuteEventContent.googlyEyes;
266 2 : return await args.room.sendEvent(
267 : content,
268 1 : inReplyTo: args.inReplyTo,
269 1 : editEventId: args.editEventId,
270 1 : txid: args.txid,
271 : );
272 : });
273 40 : addCommand('cuddle', (CommandArgs args) async {
274 1 : final content = CuteEventContent.cuddle;
275 2 : return await args.room.sendEvent(
276 : content,
277 1 : inReplyTo: args.inReplyTo,
278 1 : editEventId: args.editEventId,
279 1 : txid: args.txid,
280 : );
281 : });
282 39 : addCommand('sendRaw', (args) async {
283 0 : await args.room.sendEvent(
284 0 : jsonDecode(args.msg),
285 0 : inReplyTo: args.inReplyTo,
286 0 : txid: args.txid,
287 : );
288 : return null;
289 : });
290 39 : addCommand('ignore', (args) async {
291 0 : final mxid = args.msg;
292 0 : if (mxid.isEmpty) {
293 : throw 'Please provide a User ID';
294 : }
295 0 : await ignoreUser(mxid);
296 : return null;
297 : });
298 39 : addCommand('unignore', (args) async {
299 0 : final mxid = args.msg;
300 0 : if (mxid.isEmpty) {
301 : throw 'Please provide a User ID';
302 : }
303 0 : await unignoreUser(mxid);
304 : return null;
305 : });
306 : }
307 : }
308 :
309 : class CommandArgs {
310 : String msg;
311 : String? editEventId;
312 : Event? inReplyTo;
313 : Room room;
314 : String? txid;
315 : String? threadRootEventId;
316 : String? threadLastEventId;
317 :
318 5 : CommandArgs({
319 : required this.msg,
320 : this.editEventId,
321 : this.inReplyTo,
322 : required this.room,
323 : this.txid,
324 : this.threadRootEventId,
325 : this.threadLastEventId,
326 : });
327 : }
|