/
MatrixHandler.js
1576 lines (1420 loc) · 60.4 KB
/
MatrixHandler.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*eslint no-invalid-this: 0 consistent-return: 0*/
"use strict";
var Promise = require("bluebird");
var stats = require("../config/stats");
var MatrixRoom = require("matrix-appservice-bridge").MatrixRoom;
var IrcRoom = require("../models/IrcRoom");
var MatrixAction = require("../models/MatrixAction");
var IrcAction = require("../models/IrcAction");
var IrcClientConfig = require("../models/IrcClientConfig");
var MatrixUser = require("matrix-appservice-bridge").MatrixUser;
var BridgeRequest = require("../models/BridgeRequest");
var toIrcLowerCase = require("../irc/formatting").toIrcLowerCase;
var StateLookup = require('matrix-appservice-bridge').StateLookup;
const MSG_PMS_DISABLED = "[Bridge] Sorry, PMs are disabled on this bridge.";
const MSG_PMS_DISABLED_FEDERATION = "[Bridge] Sorry, PMs are disabled on " +
"this bridge over federation.";
const KICK_RETRY_DELAY_MS = 15000;
const KICK_DELAY_JITTER = 30000;
const EVENT_CACHE_SIZE = 4096;
function MatrixHandler(ircBridge) {
this.ircBridge = ircBridge;
// maintain a list of room IDs which are being processed invite-wise. This is
// required because invites are processed asyncly, so you could get invite->msg
// and the message is processed before the room is created.
this._processingInvitesForRooms = {
// roomId+userId: defer
};
this._memberTracker = null;
this._eventCache = new Map(); //eventId => {body, sender}
}
// ===== Matrix Invite Handling =====
/**
* Process a Matrix invite event for an Admin room.
* @param {Object} event : The Matrix invite event.
* @param {Request} req : The request for this event.
* @param {MatrixUser} inviter : The user who invited the bot.
* @param {MatrixUser} botUser : The bot itself.
*/
MatrixHandler.prototype._handleAdminRoomInvite = Promise.coroutine(function*(req, event,
inviter, botUser) {
req.log.info("Handling invite from user directed to bot.");
// Real MX user inviting BOT to a private chat
let mxRoom = new MatrixRoom(event.room_id);
yield this.ircBridge.getAppServiceBridge().getIntent().join(event.room_id);
// Do not create an admin room if the room is marked as 'plumbed'
let matrixClient = this.ircBridge.getAppServiceBridge().getClientFactory().getClientAs();
try {
let plumbedState = yield matrixClient.getStateEvent(event.room_id, 'm.room.plumbing');
if (plumbedState.status === "enabled") {
req.log.info(
'This room is marked for plumbing (m.room.plumbing.status = "enabled"). ' +
'Not treating room as admin room.'
);
return Promise.resolve();
}
}
catch (err) {
req.log.info(`Not a plumbed room: Error retrieving m.room.plumbing (${err.data.error})`);
}
// clobber any previous admin room ID
yield this.ircBridge.getStore().storeAdminRoom(mxRoom, inviter.userId);
});
/**
* Process a Matrix invite event for an Admin room.
* @param {Object} event : The Matrix invite event.
* @param {Request} req : The request for this event.
* @param {IrcUser} invitedIrcUser : The IRC user the bot invited to a room.
*/
MatrixHandler.prototype._handleInviteFromBot = Promise.coroutine(function*(req, event,
invitedIrcUser) {
req.log.info("Handling invite from bot directed at %s on %s",
invitedIrcUser.server.domain, invitedIrcUser.nick);
// Bot inviting VMX to a matrix room which is mapped to IRC. Just make a
// matrix user and join the room (we trust the bot, so no additional checks)
let mxUser = yield this.ircBridge.getMatrixUser(invitedIrcUser);
yield this.ircBridge.getAppServiceBridge().getIntent(mxUser.getId()).join(event.room_id);
});
MatrixHandler.prototype._handleInviteFromUser = Promise.coroutine(function*(req, event,
invitedIrcUser) {
req.log.info("Handling invite from user directed at %s on %s",
invitedIrcUser.server.domain, invitedIrcUser.nick);
const invitedUser = yield this.ircBridge.getMatrixUser(invitedIrcUser);
const mxRoom = new MatrixRoom(event.room_id);
const intent = this.ircBridge.getAppServiceBridge().getIntent(invitedUser.getId());
// Real MX user inviting VMX to a matrix room for PM chat
if (!invitedIrcUser.server.allowsPms()) {
req.log.error("Accepting invite, and then leaving: This server does not allow PMs.");
yield intent.join(event.room_id);
yield this.ircBridge.sendMatrixAction(mxRoom, invitedUser, new MatrixAction(
"notice",
MSG_PMS_DISABLED
), req);
yield intent.leave(event.room_id);
return;
}
// If no federated PMs are allowed, check the origin of the PM
// is same the domain as the bridge
if (!invitedIrcUser.server.shouldFederatePMs()) {
// Matches for the local part (the not-user part)
var localpart = event.user_id.match(/[^:]*:(.*)/)[1];
if (localpart !== this.ircBridge.domain) {
req.log.error(
"Accepting invite, and then leaving: This server does not allow federated PMs."
);
yield intent.join(event.room_id);
yield this.ircBridge.sendMatrixAction(mxRoom, invitedUser, new MatrixAction(
"notice",
MSG_PMS_DISABLED_FEDERATION
), req);
yield intent.leave(event.room_id);
return;
}
req.log.info("(PM federation)Invite not rejected: user on local HS");
}
else {
req.log.info("(PM federation)Invite not rejected: federated PMs allowed");
}
// create a virtual Matrix user for the IRC user
yield intent.join(event.room_id);
req.log.info("Joined %s to room %s", invitedUser.getId(), event.room_id);
// check if this room is a PM room or not.
let roomState = yield intent.roomState(event.room_id);
let joinedMembers = roomState.filter((ev) => {
return ev.type === "m.room.member" && ev.content.membership === "join";
}).map((ev) => ev.state_key);
let isPmRoom = (
joinedMembers.length === 2 && joinedMembers.indexOf(event.user_id) !== -1
);
if (isPmRoom) {
// nick is the channel
let ircRoom = new IrcRoom(
invitedIrcUser.server, invitedIrcUser.nick
);
yield this.ircBridge.getStore().setPmRoom(
ircRoom, mxRoom, event.user_id, event.state_key
);
return;
}
req.log.error("This room isn't a 1:1 chat!");
// whine that you don't do group chats and leave.
let notice = new MatrixAction("notice",
"Group chat not supported."
);
try {
yield this.ircBridge.sendMatrixAction(mxRoom, invitedUser, notice, req);
}
catch (err) {
// ignore, we want to leave the room regardless.
}
yield intent.leave(
event.room_id
);
});
// === Admin room handling ===
MatrixHandler.prototype._onAdminMessage = Promise.coroutine(function*(req, event, adminRoom) {
req.log.info("Received admin message from %s", event.user_id);
let botUser = new MatrixUser(this.ircBridge.getAppServiceUserId());
// If an admin room has more than 2 people in it, kick the bot out
let members = [];
if (this._memberTracker) {
// First call begins tracking, subsequent calls do nothing
yield this._memberTracker.trackRoom(adminRoom.getId());
members = this._memberTracker.getState(
adminRoom.getId(),
'm.room.member'
).filter(
function (m) {
return m.content.membership && m.content.membership === "join";
}
);
}
else {
req.log.warn('Member tracker not running');
}
if (members.length > 2) {
req.log.error(
`_onAdminMessage: admin room has ${members.length}` +
` users instead of just 2; bot will leave`
);
// Notify users in admin room
let notice = new MatrixAction("notice",
"There are more than 2 users in this admin room"
);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
yield this.ircBridge.getAppServiceBridge().getIntent(
botUser.getId()
).leave(adminRoom.getId());
return;
}
// Assumes all commands have the form "!wxyz [irc.server] [args...]"
let segments = event.content.body.split(" ");
let cmd = segments.shift();
let args = segments;
if (cmd === "!help") {
let helpCommands = {
"!join": {
example: `!join [irc.example.net] #channel [key]`,
summary: `Join a channel (with optional channel key)`,
},
"!nick": {
example: `!nick [irc.example.net] DesiredNick`,
summary: "Change your nick. If no arguments are supplied, " +
"your current nick is shown.",
},
"!whois": {
example: `!whois [irc.example.net] NickName|@alice:matrix.org`,
summary: "Do a /whois lookup. If a Matrix User ID is supplied, " +
"return information about that user's IRC connection.",
},
"!storepass": {
example: `!storepass [irc.example.net] passw0rd`,
summary: `Store a NickServ password (server password)`,
},
"!removepass": {
example: `!removepass [irc.example.net]`,
summary: `Remove a previously stored NickServ password`,
},
"!quit": {
example: `!quit`,
summary: "Leave all bridged channels, on all networks, and remove your " +
"connections to all networks.",
},
"!cmd": {
example: `!cmd [irc.example.net] COMMAND [arg0 [arg1 [...]]]`,
summary: "Issue a raw IRC command. These will not produce a reply." +
"(Note that the command must be all uppercase.)",
},
};
let notice = new MatrixAction("notice", null,
`This is an IRC admin room for controlling your IRC connection and sending ` +
`commands directly to IRC. ` +
`The following commands are available:<br/><ul>\n\t` +
Object.keys(helpCommands).map((c) => {
return (
`<li>` +
`<strong>${helpCommands[c].example}</strong> : ${helpCommands[c].summary}` +
`</li>`
);
}).join(`\n\t`) +
`</ul>`
);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
// Work out which IRC server the command is directed at.
let clientList = this.ircBridge.getBridgedClientsForUserId(event.user_id);
let ircServer = this.ircBridge.getServer(args[0]);
if (ircServer) {
args.shift(); // pop the server so commands don't need to know
}
else {
// default to the server the client is connected to if there is only one
if (clientList.length === 1) {
ircServer = clientList[0].server;
}
// default to the only server we know about if we only bridge 1 thing.
else if (this.ircBridge.getServers().length === 1) {
ircServer = this.ircBridge.getServers()[0];
}
else {
let notice = new MatrixAction("notice",
"A server address must be specified."
);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
}
if (cmd === "!nick") {
// Format is: "!nick irc.example.com NewNick"
if (!ircServer.allowsNickChanges()) {
let notice = new MatrixAction("notice",
"Server " + ircServer.domain + " does not allow nick changes."
);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
let nick = args.length === 1 ? args[0] : null; // make sure they only gave 1 arg
if (!ircServer || !nick) {
let connectedNetworksStr = "";
if (clientList.length === 0) {
connectedNetworksStr = (
"You are not currently connected to any " +
"IRC networks which have nick changes enabled."
);
}
else {
connectedNetworksStr = "Currently connected to IRC networks:\n";
for (let i = 0; i < clientList.length; i++) {
connectedNetworksStr += clientList[i].server.domain +
" as " + clientList[i].nick + "\n";
}
}
let notice = new MatrixAction("notice",
"Format: '!nick DesiredNick' or '!nick irc.server.name DesiredNick'\n" +
connectedNetworksStr
);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
req.log.info("%s wants to change their nick on %s to %s",
event.user_id, ircServer.domain, nick);
if (ircServer.claimsUserId(event.user_id)) {
req.log.error("%s is a virtual user!", event.user_id);
return BridgeRequest.ERR_VIRTUAL_USER;
}
// change the nick
let bridgedClient = yield this.ircBridge.getBridgedClient(ircServer, event.user_id);
try {
let response = yield bridgedClient.changeNick(nick, true);
let noticeRes = new MatrixAction("notice", response);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, noticeRes, req);
// persist this desired nick
let config = yield this.ircBridge.getStore().getIrcClientConfig(
event.user_id, ircServer.domain
);
if (!config) {
config = IrcClientConfig.newConfig(
bridgedClient.matrixUser, ircServer.domain, nick
);
}
config.setDesiredNick(nick);
yield this.ircBridge.getStore().storeIrcClientConfig(config);
return;
}
catch (err) {
if (err.stack) {
req.log.error(err);
}
let noticeErr = new MatrixAction("notice", err.message);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, noticeErr, req);
return;
}
}
else if (cmd === "!join") {
// TODO: Code dupe from !nick
// Format is: "!join irc.example.com #channel [key]"
// check that the server exists and that the user_id is on the whitelist
let ircChannel = args[0];
let key = args[1]; // keys can't have spaces in them, so we can just do this.
let errText = null;
if (!ircChannel || ircChannel.indexOf("#") !== 0) {
errText = "Format: '!join irc.example.com #channel [key]'";
}
else if (ircServer.hasInviteRooms() && !ircServer.isInWhitelist(event.user_id)) {
errText = "You are not authorised to join channels on this server.";
}
if (errText) {
yield this.ircBridge.sendMatrixAction(
adminRoom, botUser, new MatrixAction("notice", errText), req
);
return;
}
req.log.info("%s wants to join the channel %s on %s", event.user_id,
ircChannel, ircServer.domain);
// There are 2 main flows here:
// - The !join is instigated to make the BOT join a new channel.
// * Bot MUST join and invite user
// - The !join is instigated to make the USER join a new channel.
// * IRC User MAY have to join (if bridging incr joins or using a chan key)
// * Bot MAY invite user
//
// This means that in both cases:
// 1) Bot joins IRC side (NOP if bot is disabled)
// 2) Bot sends Matrix invite to bridged room. (ignore failures if already in room)
// And *sometimes* we will:
// 3) Force join the IRC user (if given key / bridging joins)
// track the channel if we aren't already
let matrixRooms = yield this.ircBridge.getStore().getMatrixRoomsForChannel(
ircServer, ircChannel
);
if (matrixRooms.length === 0) {
// track the channel then invite them.
// TODO: Dupes onAliasQuery a lot
const initial_state = [
{
type: "m.room.join_rules",
state_key: "",
content: {
join_rule: ircServer.getJoinRule()
}
},
{
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: "joined"
}
}
];
if (ircServer.areGroupsEnabled()) {
initial_state.push({
type: "m.room.related_groups",
state_key: "",
content: {
groups: [ircServer.getGroupId()]
}
});
}
let ircRoom = yield this.ircBridge.trackChannel(ircServer, ircChannel, key);
let response = yield this.ircBridge.getAppServiceBridge().getIntent(
event.user_id
).createRoom({
options: {
name: ircChannel,
visibility: "private",
preset: "public_chat",
creation_content: {
"m.federate": ircServer.shouldFederate()
},
initial_state
}
});
let mxRoom = new MatrixRoom(response.room_id);
yield this.ircBridge.getStore().storeRoom(
ircRoom, mxRoom, 'join'
);
// /mode the channel AFTER we have created the mapping so we process
// +s and +i correctly.
this.ircBridge.publicitySyncer.initModeForChannel(ircServer, ircChannel).catch(
(err) => {
log.error(
`Could not init mode for channel ${ircChannel} on ${ircServer.domain}`
);
});
req.log.info(
"Created a room to track %s on %s and invited %s",
ircRoom.channel, ircServer.domain, event.user_id
);
matrixRooms.push(mxRoom);
}
// already tracking channel, so just invite them.
let invitePromises = matrixRooms.map((room) => {
req.log.info(
"Inviting %s to room %s", event.user_id, room.getId()
);
return this.ircBridge.getAppServiceBridge().getIntent().invite(
room.getId(), event.user_id
);
});
// check whether we should be force joining the IRC user
for (let i = 0; i < matrixRooms.length; i++) {
let m = matrixRooms[i];
let userMustJoin = (
key || ircServer.shouldSyncMembershipToIrc("incremental", m.getId())
);
if (userMustJoin) {
// force join then break out (we only ever join once no matter how many
// rooms the channel is bridged to)
let bc = yield this.ircBridge.getBridgedClient(
ircServer, event.user_id
);
yield bc.joinChannel(ircChannel, key);
break;
}
}
yield Promise.all(invitePromises);
}
else if (cmd === "!whois") {
// Format is: "!whois <nick>"
let whoisNick = args.length === 1 ? args[0] : null; // ensure 1 arg
if (!whoisNick) {
yield this.ircBridge.sendMatrixAction(
adminRoom, botUser,
new MatrixAction("notice", "Format: '!whois nick|mxid'"), req
);
return;
}
if (whoisNick[0] === "@") {
// querying a Matrix user - whoisNick is the matrix user ID
req.log.info("%s wants whois info on %s", event.user_id, whoisNick);
let whoisClient = this.ircBridge.getIrcUserFromCache(ircServer, whoisNick);
try {
let noticeRes = new MatrixAction(
"notice",
whoisClient ?
`${whoisNick} is connected to ${ircServer.domain} as '${whoisClient.nick}'.` :
`${whoisNick} has no IRC connection via this bridge.`);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, noticeRes, req);
}
catch (err) {
if (err.stack) {
req.log.error(err);
}
let noticeErr = new MatrixAction("notice", "Failed to perform whois query.");
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, noticeErr, req);
}
return;
}
req.log.info("%s wants whois info on %s on %s", event.user_id,
whoisNick, ircServer.domain);
let bridgedClient = yield this.ircBridge.getBridgedClient(ircServer, event.user_id);
try {
let response = yield bridgedClient.whois(whoisNick);
let noticeRes = new MatrixAction("notice", response.msg);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, noticeRes, req);
}
catch (err) {
if (err.stack) {
req.log.error(err);
}
let noticeErr = new MatrixAction("notice", err.message);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, noticeErr, req);
}
return;
}
else if (cmd === "!quit") {
const msgText = yield this.quitUser(
req, event.user_id, clientList, ircServer, "issued !quit command"
);
if (msgText) {
let notice = new MatrixAction("notice", msgText);
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
}
return;
}
else if (cmd === "!storepass") {
let domain = ircServer.domain;
let userId = event.user_id;
let notice;
try {
// Allow passwords with spaces
let pass = args.join('');
let explanation = `When you next reconnect to ${domain}, this password ` +
`will be automatically sent in a PASS command which most ` +
`IRC networks will use as your NickServ password. This ` +
`means you will not need to talk to NickServ. This does ` +
`NOT apply to your currently active connection: you still ` +
`need to talk to NickServ one last time to authenticate ` +
`your current connection if you haven't already.`;
if (pass.length === 0) {
notice = new MatrixAction(
"notice",
"Format: '!storepass password' " +
"or '!storepass irc.server.name password'\n" + explanation
);
}
else {
yield this.ircBridge.getStore().storePass(userId, domain, pass);
notice = new MatrixAction(
"notice", `Successfully stored password on ${domain}. ` + explanation
);
}
}
catch (err) {
notice = new MatrixAction(
"notice", `Failed to store password: ${err.message}`
);
req.log.error(err.stack);
}
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
else if (cmd === "!removepass") {
let domain = ircServer.domain;
let userId = event.user_id;
let notice;
try {
yield this.ircBridge.getStore().removePass(userId, domain);
notice = new MatrixAction(
"notice", `Successfully removed password.`
);
}
catch (err) {
notice = new MatrixAction(
"notice", `Failed to remove password: ${err.message}`
);
}
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
else if (cmd === "!cmd" && args[0]) {
req.log.info(`No valid (old form) admin command, will try new format`);
// Assumes commands have the form
// !cmd [irc.server] COMMAND [arg0 [arg1 [...]]]
let currentServer = ircServer;
let blacklist = ['PROTOCTL'];
try {
let keyword = args[0];
// keyword could be a failed server or a malformed command
if (!keyword.match(/^[A-Z]+$/)) {
// if not a domain OR is only word (which implies command)
if (!keyword.match(/^[a-z0-9:\.-]+$/) || args.length == 1) {
throw new Error(`Malformed command: ${keyword}`);
}
else {
throw new Error(`Domain not accepted: ${keyword}`);
}
}
if (blacklist.indexOf(keyword) != -1) {
throw new Error(`Command blacklisted: ${keyword}`);
}
// If no args after COMMAND, this will be []
let sendArgs = args.splice(1);
sendArgs.unshift(keyword);
let bridgedClient = yield this.ircBridge.getBridgedClient(
currentServer, event.user_id
);
if (!bridgedClient.unsafeClient) {
throw new Error('Possibly disconnected');
}
bridgedClient.unsafeClient.send.apply(bridgedClient.unsafeClient, sendArgs);
}
catch (err) {
let notice = new MatrixAction("notice", `${err}\n` );
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
}
else {
let notice = new MatrixAction("notice",
"The command was not recognised. Available commands are listed by !help");
yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
});
MatrixHandler.prototype.quitUser = Promise.coroutine(function*(req, userId, clientList,
ircServer, reason) {
let clients = clientList;
if (ircServer) {
// Filter to get the clients for the [specified] server
clients = clientList.filter(
(bridgedClient) => bridgedClient.server.domain === ircServer.domain
);
}
if (clients.length === 0) {
req.log.info(`No bridgedClients for ${userId}`);
return "You are not connected to any networks.";
}
for (let i = 0; i < clients.length; i++) {
const bridgedClient = clients[i];
if (bridgedClient.chanList.length === 0) {
req.log.info(
`Bridged client for ${userId} is not in any channels ` +
`on ${bridgedClient.server.domain}`
);
}
else {
// Get all rooms that the bridgedClient is in
let rooms = yield Promise.all(
bridgedClient.chanList.map(
(channel) => {
return this.ircBridge.getStore().getMatrixRoomsForChannel(
bridgedClient.server, channel
);
}
)
);
// rooms is an array of arrays
rooms = rooms.reduce((a, b) => {return a.concat(b)});
let uniqueRoomIds = Array.from(
new Set(rooms.map((matrixRoom) => matrixRoom.roomId))
);
for (let j = 0; j < uniqueRoomIds.length; j++) {
let roomId = uniqueRoomIds[j];
try {
yield this.ircBridge.getAppServiceBridge().getIntent().kick(
roomId, bridgedClient.userId, reason
);
}
catch (err) {
req.log.error(err);
req.log.warn(
`Could not kick ${bridgedClient.userId} ` +
`from bridged room ${roomId}: ${err.message}`
);
}
}
}
req.log.info(
`Killing bridgedClient (nick = ${bridgedClient.nick}) for ${bridgedClient.userId}`
);
// The success message will effectively be 'Your connection to ... has been lost.`
bridgedClient.kill(reason);
}
return null;
});
/**
* Called when the AS receives a new Matrix invite/join/leave event.
* @param {Object} event : The Matrix member event.
*/
MatrixHandler.prototype._onMemberEvent = function(req, event) {
if (!this._memberTracker) {
let matrixClient = this.ircBridge.getAppServiceBridge().getClientFactory().getClientAs();
this._memberTracker = new StateLookup({
client : matrixClient,
eventTypes: ['m.room.member']
});
}
else {
this._memberTracker.onEvent(event);
}
};
/**
* Called when the AS receives a new Matrix invite event.
* @param {Object} event : The Matrix invite event.
* @param {MatrixUser} inviter : The inviter (sender).
* @param {MatrixUser} invitee : The invitee (receiver).
* @return {Promise} which is resolved/rejected when the request finishes.
*/
MatrixHandler.prototype._onInvite = Promise.coroutine(function*(req, event, inviter, invitee) {
/*
* (MX=Matrix user, VMX=Virtual matrix user, BOT=AS bot)
* Valid invite flows:
* [1] MX --invite--> VMX (starting a PM chat)
* [2] bot --invite--> VMX (invite-only room that the bot is in who is inviting virtuals)
* [3] MX --invite--> BOT (admin room; auth)
* [4] bot --invite--> MX (bot telling real mx user IRC conn state) - Ignore.
* [5] irc --invite--> MX (real irc user PMing a Matrix user) - Ignore.
*/
req.log.info("onInvite: %s", JSON.stringify(event));
this._onMemberEvent(req, event);
// mark this room as being processed in case we simultaneously get
// messages for this room (which would fail if we haven't done the
// invite yet!)
this._processingInvitesForRooms[event.room_id + event.state_key] = req.getPromise();
req.getPromise().finally(() => {
delete this._processingInvitesForRooms[event.room_id + event.state_key];
});
// work out which flow we're dealing with and fork off asap
// is the invitee the bot?
if (this.ircBridge.getAppServiceUserId() === event.state_key) {
// case [3]
yield this._handleAdminRoomInvite(req, event, inviter, invitee);
}
// else is the invitee a real matrix user? If they are, there will be no IRC server
else if (!this.ircBridge.getServerForUserId(event.state_key)) {
// cases [4] and [5] : We cannot accept on behalf of real matrix users, so nop
return BridgeRequest.ERR_NOT_MAPPED;
}
else {
// cases [1] and [2] : The invitee represents a real IRC user
let ircUser = yield this.ircBridge.matrixToIrcUser(invitee);
// is the invite from the bot?
if (this.ircBridge.getAppServiceUserId() === event.user_id) {
yield this._handleInviteFromBot(req, event, ircUser); // case [2]
}
else {
yield this._handleInviteFromUser(req, event, ircUser); // case [1]
}
}
});
MatrixHandler.prototype._onJoin = Promise.coroutine(function*(req, event, user) {
let self = this;
req.log.info("onJoin: %s", JSON.stringify(event));
this._onMemberEvent(req, event);
// membershiplists injects leave events when syncing initial membership
// lists. We know if this event is injected because this flag is set.
let syncKind = event._injected ? "initial" : "incremental";
let promises = []; // one for each join request
if (this.ircBridge.getAppServiceUserId() === user.getId()) {
// ignore messages from the bot
return BridgeRequest.ERR_VIRTUAL_USER;
}
// is this a tracked channel?
let ircRooms = yield this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id);
// =========== Bridge Bot Joining ===========
// Make sure the bot is joining on all mapped IRC channels
ircRooms.forEach((ircRoom) => {
this.ircBridge.joinBot(ircRoom);
});
// =========== Client Joining ===========
// filter out rooms which don't mirror matrix join parts and are NOT frontier
// entries. Frontier entries must ALWAYS be joined else the IRC channel will
// not be bridged!
ircRooms = ircRooms.filter(function(room) {
return room.server.shouldSyncMembershipToIrc(
syncKind, event.room_id
) || event._frontier;
});
if (ircRooms.length === 0) {
req.log.info(
"No tracked channels which mirror joins for this room."
);
return BridgeRequest.ERR_NOT_MAPPED;
}
// for each room (which may be on different servers)
ircRooms.forEach(function(room) {
if (room.server.claimsUserId(user.getId())) {
req.log.info("%s is a virtual user (claimed by %s)",
user.getId(), room.server.domain);
return;
}
// get the virtual IRC user for this user
promises.push(Promise.coroutine(function*() {
let bridgedClient;
let kickIntent;
try {
bridgedClient = yield self.ircBridge.getBridgedClient(
room.server, user.getId(), (event.content || {}).displayname
);
}
catch (e) {
// We need to kick on failure to get a client.
req.log.info(`${user.getId()} failed to get a IRC connection. Kicking from room.`);
kickIntent = self.ircBridge.getAppServiceBridge().getIntent();
}
while (kickIntent) {
try {
yield kickIntent.kick(
event.room_id, user.getId(),
`Connection limit reached for ${room.server.domain}. Please try again later.`
);
break;
}
catch (err) {
const delay = KICK_RETRY_DELAY_MS + (Math.random() * KICK_DELAY_JITTER);
req.log.warn(
`User was not kicked. Retrying in ${delay}ms. ${err}`
);
yield Promise.delay(delay);
}
}
// Check for a displayname change and update nick accordingly.
if (event.content.displayname !== bridgedClient.displayName) {
bridgedClient.displayName = event.content.displayname;
// Changing the nick requires that:
// - the server allows nick changes
// - the nick is not custom
let config = yield self.ircBridge.getStore().getIrcClientConfig(
bridgedClient.userId, room.server.domain
);
if (room.server.allowsNickChanges() &&
config.getDesiredNick() === null
) {
bridgedClient.changeNick(
room.server.getNick(bridgedClient.userId, event.content.displayname),
false);
}
}
yield bridgedClient.joinChannel(room.channel); // join each channel
})());
});
// We know ircRooms.length > 1. The only time when this isn't mapped into a Promise
// is when there is a virtual user: TODO: clean this up! Control flow is hard.
if (promises.length === 0) {
return BridgeRequest.ERR_VIRTUAL_USER;
}
stats.membership(false, "join");
yield Promise.all(promises);
});
MatrixHandler.prototype._onKick = Promise.coroutine(function*(req, event, kicker, kickee) {
req.log.info(
"onKick %s is kicking/banning %s from %s",
kicker.getId(), kickee.getId(), event.room_id
);
this._onMemberEvent(req, event);
/*
We know this is a Matrix client kicking someone.
There are 2 scenarios to consider here:
- Matrix on Matrix kicking
- Matrix on IRC kicking
Matrix-Matrix
=============
__USER A____ ____USER B___
| | | |
Matrix vIRC1 Matrix vIRC2 | Effect
-----------------------------------------------------------------------
Kicker Kickee | vIRC2 parts channel.
This avoids potential permission issues
in case vIRC1 cannot kick vIRC2 on IRC.
Matrix-IRC
==========
__USER A____ ____USER B___
| | | |
Matrix vIRC IRC vMatrix | Effect
-----------------------------------------------------------------------
Kicker Kickee | vIRC tries to kick IRC via KICK command.
*/
let ircRooms = yield this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id);
// do we have an active connection for the kickee? This tells us if they are real
// or virtual.
let kickeeClients = this.ircBridge.getBridgedClientsForUserId(kickee.getId());
if (kickeeClients.length === 0) {
// Matrix on IRC kicking, work out which IRC user to kick.
let server = null;
for (let i = 0; i < ircRooms.length; i++) {
if (ircRooms[i].server.claimsUserId(kickee.getId())) {
server = ircRooms[i].server;
break;
}
}
if (!server) {
return; // kicking a bogus user
}
let kickeeNick = server.getNickFromUserId(kickee.getId());
if (!kickeeNick) {
return; // bogus virtual user ID
}
// work out which client will do the kicking
let kickerClient = this.ircBridge.getIrcUserFromCache(server, kicker.getId());
if (!kickerClient) {
// well this is awkward.. whine about it and bail.
req.log.error(
"%s has no client instance to send kick from. Cannot kick.",
kicker.getId()
);
return;
}
// we may be bridging this matrix room into many different IRC channels, and we want
// to kick this user from all of them.
for (let i = 0; i < ircRooms.length; i++) {
if (ircRooms[i].server.domain !== server.domain) {
return;
}
kickerClient.kick(
kickeeNick, ircRooms[i].channel,
`Kicked by ${kicker.getId()}` +
(event.content.reason ? ` : ${event.content.reason}` : "")
);
}
}