diff --git a/changelog.d/1807.misc b/changelog.d/1807.misc new file mode 100644 index 000000000..d6eaa3713 --- /dev/null +++ b/changelog.d/1807.misc @@ -0,0 +1,7 @@ +Change format for file uploads and codeblocks +Change default format for long replies +Handle replying to self +Prefix nicknames with "`" instead of "M" when they start with an invalid character +Fix long message replies +In `!cmd`, don't require commands be all uppercase +Initial support for bridging bans to IRC diff --git a/config.sample.yaml b/config.sample.yaml index 84ab35cba..d95bfbca4 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -339,7 +339,7 @@ ircService: ircClients: # The template to apply to every IRC client nick. This MUST have either # $DISPLAY or $USERID or $LOCALPART somewhere in it. - # Optional. Default: "M-$DISPLAY". Example: "M-Alice". + # Optional. Default: "$DISPLAY[m]". Example: "Alice[m]". nickTemplate: "$DISPLAY[m]" # True to allow virtual IRC clients to change their nick on this server # by issuing !nick commands to the IRC AS bot. @@ -601,7 +601,9 @@ ircService: # format of replies sent shortly after the original message shortReplyTemplate: "$NICK: $REPLY" # format of replies sent a while after the original message - longReplyTemplate: "<$NICK> \"$ORIGINAL\" <- $REPLY" + longReplyTemplate: "$NICK: \"$ORIGINAL\" <- $REPLY" + # format of replies where the sender of the original message is the same as the sender of the reply + selfReplyTemplate: "<$NICK> $ORIGINAL\n$REPLY" # how much time needs to pass between the reply and the original message to switch to the long format shortReplyTresholdSeconds: 300 # Ignore users mentioned in a io.element.functional_members state event when checking admin room membership diff --git a/config.schema.yml b/config.schema.yml index 17d78fe2a..6bedeaf03 100644 --- a/config.schema.yml +++ b/config.schema.yml @@ -171,6 +171,8 @@ properties: type: "string" shortReplyTresholdSeconds: type: "integer" + selfReplyTemplate: + type: "string" ignoreFunctionalMembersInAdminRooms: type: "boolean" ircHandler: diff --git a/spec/integ/admin-rooms.spec.js b/spec/integ/admin-rooms.spec.js index 1f2124c08..c89f21732 100644 --- a/spec/integ/admin-rooms.spec.js +++ b/spec/integ/admin-rooms.spec.js @@ -885,14 +885,12 @@ describe("Admin rooms", function() { cmdIx++; }); - // 5 commands should be executed - // rubbishserver should not be accepted + // 4 commands should be executed const commands = [ `!cmd ${roomMapping.server} JOIN ${newChannel}`, `!cmd ${roomMapping.server} TOPIC ${newChannel} :some new fancy topic`, `!cmd ${roomMapping.server} PART ${newChannel}`, - `!cmd ${roomMapping.server} STUPID COMMANDS`, - `!cmd rubbishserver SOME COMMAND`]; + `!cmd ${roomMapping.server} STUPID COMMANDS`]; for (let i = 0; i < commands.length; i++) { // send commands diff --git a/spec/integ/kicking.spec.js b/spec/integ/kicking.spec.js index 56aa31ea9..f3ec39098 100644 --- a/spec/integ/kicking.spec.js +++ b/spec/integ/kicking.spec.js @@ -176,6 +176,118 @@ describe("Kicking", () => { }); +describe("Banning", () => { + + const {env, config, test} = envBundle(); + + const mxUser = { + id: "@flibble:wibble", + nick: "M-flibble" + }; + + const ircUser = { + nick: "bob", + localpart: config._server + "_bob", + id: `@${config._server}_bob:${config.homeserver.domain}` + }; + + const ircUserKicker = { + nick: "KickerNick", + localpart: config._server + "_KickerNick", + id: "@" + config._server + "_KickerNick:" + config.homeserver.domain + }; + + beforeEach(async () => { + await test.beforeEach(env); + + // accept connection requests from eeeeeeeeveryone! + env.ircMock._autoConnectNetworks( + config._server, mxUser.nick, config._server + ); + env.ircMock._autoConnectNetworks( + config._server, ircUser.nick, config._server + ); + env.ircMock._autoConnectNetworks( + config._server, config._botnick, config._server + ); + // accept join requests from eeeeeeeeveryone! + env.ircMock._autoJoinChannels( + config._server, mxUser.nick, config._chan + ); + env.ircMock._autoJoinChannels( + config._server, ircUser.nick, config._chan + ); + env.ircMock._autoJoinChannels( + config._server, config._botnick, config._chan + ); + + // we also don't care about registration requests for the irc user + env.clientMock._intent(ircUser.id)._onHttpRegister({ + expectLocalpart: ircUser.localpart, + returnUserId: ircUser.id + }); + + await test.initEnv(env); + + // make the matrix user be on IRC + await env.mockAppService._trigger("type:m.room.message", { + content: { + body: "let me in", + msgtype: "m.text" + }, + user_id: mxUser.id, + room_id: config._roomid, + type: "m.room.message" + }); + const botIrcClient = await env.ircMock._findClientAsync(config._server, config._botnick); + // make the IRC user be on Matrix + botIrcClient.emit("message", ircUser.nick, config._chan, "let me in"); + }); + + afterEach(async () => test.afterEach(env)); + + describe("IRC users on Matrix", () => { + it("should make the virtual IRC client set MODE +b and KICK the real IRC user", async () => { + let reason = "Get some help."; + let userBannedPromise = new Promise(function(resolve, reject) { + env.ircMock._whenClient(config._server, mxUser.nick, "send", + function(client, cmd, chan, arg1, arg2) { + expect(client.nick).toEqual(mxUser.nick); + expect(client.addr).toEqual(config._server); + expect(chan).toEqual(config._chan); + if (cmd !== "KICK") { + // We sent a MODE + expect(cmd).toEqual("MODE"); + expect(arg1).toEqual("+b"); // mode +b => ban + expect(arg2).toEqual(`${ircUser.nick}!*@*`); // argument to +b + } + else { + expect(cmd).toEqual("KICK"); + expect(arg1).toEqual(ircUser.nick); // nick + expect(arg2.indexOf(reason)).not.toEqual(-1, // kick reason + `kick reason was not mirrored to IRC. Got '${arg2}', + expected '${reason}'.`); + } + resolve(); + }); + }); + + await env.mockAppService._trigger("type:m.room.member", { + content: { + reason: reason, + membership: "ban" + }, + user_id: mxUser.id, + state_key: ircUser.id, + room_id: config._roomid, + type: "m.room.member" + }); + await userBannedPromise; + }); + }); +}); + + describe("Kicking on IRC join", () => { const {env, config, test} = envBundle(); diff --git a/spec/integ/matrix-to-irc.spec.js b/spec/integ/matrix-to-irc.spec.js index 9aff5ede5..02ba54511 100644 --- a/spec/integ/matrix-to-irc.spec.js +++ b/spec/integ/matrix-to-irc.spec.js @@ -234,6 +234,52 @@ describe("Matrix-to-IRC message bridging", function() { }); }); + it("should bridge matrix replies to self as self-replies", async () => { + // Trigger an original event + await env.mockAppService._trigger("type:m.room.message", { + content: { + body: "This is the real message", + msgtype: "m.text" + }, + room_id: roomMapping.roomId, + sender: repliesUser.id, + event_id: "$original:bar.com", + origin_server_ts: Date.now(), + type: "m.room.message" + }); + const p = env.ircMock._whenClient(roomMapping.server, repliesUser.nick, "say", + (client, channel, text) => { + expect(client.nick).toEqual(repliesUser.nick); + expect(client.addr).toEqual(roomMapping.server); + expect(channel).toEqual(roomMapping.channel); + expect(text).toEqual(`<${repliesUser.nick}> This is the real message\nReply Text`); + } + ); + const formatted_body = constructHTMLReply( + "This is the fake message", + "@somedude:bar.com", + "Reply text" + ); + await env.mockAppService._trigger("type:m.room.message", { + content: { + body: "> <@somedude:bar.com> This is the fake message\n\nReply Text", + formatted_body, + format: "org.matrix.custom.html", + msgtype: "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$original:bar.com" + } + }, + }, + sender: repliesUser.id, + room_id: roomMapping.roomId, + origin_server_ts: Date.now(), + type: "m.room.message" + }); + await p; + }); + it("should bridge rapid matrix replies as short replies", async () => { // Trigger an original event await env.mockAppService._trigger("type:m.room.message", { @@ -298,7 +344,7 @@ describe("Matrix-to-IRC message bridging", function() { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); - expect(text).toEqual(`<${repliesUser.nick}> "This is the real message" <- Reply Text`); + expect(text).toEqual(`${repliesUser.nick}: "This is the real message" <- Reply Text`); } ); const formatted_body = constructHTMLReply( @@ -389,7 +435,7 @@ describe("Matrix-to-IRC message bridging", function() { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); - expect(text).toEqual(`<${repliesUser.nick}> "This..." <- Reply Text`); + expect(text).toEqual(`${repliesUser.nick}: "This..." <- Reply Text`); } ); const formatted_body = constructHTMLReply( @@ -499,7 +545,7 @@ describe("Matrix-to-IRC message bridging", function() { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); - expect(text).toEqual(' "Message #2" <- Message #3'); + expect(text).toEqual('M-friend: "Message #2" <- Message #3'); } ); @@ -650,7 +696,7 @@ describe("Matrix-to-IRC message bridging", function() { }); }); - it("should bridge mutliline code blocks as IRC action with URL", function(done) { + it("should bridge mutliline code blocks as a URL", function(done) { let tBody = "```javascript\n" + " expect(text.indexOf(\"javascript\")).not.toEqual(-1);\n" + @@ -662,13 +708,12 @@ describe("Matrix-to-IRC message bridging", function() { const sdk = env.clientMock._client(config._botUserId); sdk.uploadContent.and.returnValue(Promise.resolve("mxc://deadbeefcafe")); - env.ircMock._whenClient(roomMapping.server, testUser.nick, "action", (client, channel, text) => { + env.ircMock._whenClient(roomMapping.server, testUser.nick, "say", (client, channel, text) => { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); // don't be too brittle when checking this, but I expect to see the - // code type and the mxc fragment. - expect(text.indexOf('javascript')).not.toEqual(-1); + // mxc fragment. expect(text.indexOf('deadbeefcafe')).not.toEqual(-1); done(); }); @@ -713,18 +758,17 @@ describe("Matrix-to-IRC message bridging", function() { }); }); - it("should bridge matrix images as IRC action with a URL", function(done) { + it("should bridge matrix images as a URL", function(done) { const tBody = "the_image.jpg"; const tMxcSegment = "/somecontentid"; const tHsUrl = "https://some.home.server.goeshere/"; - env.ircMock._whenClient(roomMapping.server, testUser.nick, "action", (client, channel, text) => { + env.ircMock._whenClient(roomMapping.server, testUser.nick, "say", (client, channel, text) => { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); // don't be too brittle when checking this, but I expect to see the - // filename (body) and the http url. - expect(text.indexOf(tBody)).not.toEqual(-1); + // http url. expect(text.indexOf(tHsUrl)).not.toEqual(-1); expect(text.indexOf(tMxcSegment)).not.toEqual(-1); done(); @@ -742,18 +786,17 @@ describe("Matrix-to-IRC message bridging", function() { }); }); - it("should bridge matrix files as IRC action with a URL", function(done) { + it("should bridge matrix files as a URL", function(done) { const tBody = "a_file.apk"; const tMxcSegment = "/somecontentid"; const tHsUrl = "https://some.home.server.goeshere/"; - env.ircMock._whenClient(roomMapping.server, testUser.nick, "action", (client, channel, text) => { + env.ircMock._whenClient(roomMapping.server, testUser.nick, "say", (client, channel, text) => { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); // don't be too brittle when checking this, but I expect to see the - // filename (body) and the http url. - expect(text.indexOf(tBody)).not.toEqual(-1); + // http url. expect(text.indexOf(tHsUrl)).not.toEqual(-1); expect(text.indexOf(tMxcSegment)).not.toEqual(-1); done(); @@ -1084,21 +1127,20 @@ describe("Matrix-to-IRC message bridging with media URL and drop time", function expect(said).toBe(true); }); - it("should bridge matrix files as IRC action with a configured media URL", function(done) { + it("should bridge matrix files as IRC message with a configured media URL", function(done) { let tBody = "a_file.apk"; let tMxcSegment = "/somecontentid"; let tMediaUrl = mediaUrl; let tHsUrl = "http://somedomain.com"; const sdk = env.clientMock._client(config._botUserId); - env.ircMock._whenClient(roomMapping.server, testUser.nick, "action", + env.ircMock._whenClient(roomMapping.server, testUser.nick, "say", function(client, channel, text) { expect(client.nick).toEqual(testUser.nick); expect(client.addr).toEqual(roomMapping.server); expect(channel).toEqual(roomMapping.channel); // don't be too brittle when checking this, but I expect to see the - // filename (body) and the http url. - expect(text.indexOf(tBody)).not.toEqual(-1, "File name not present"); + // http url. expect(text.indexOf(tHsUrl)).toEqual(-1, "HS URL present instead of media URL"); expect(text.indexOf(tMediaUrl)).not.toEqual(-1, "No media URL"); expect(text.indexOf(tMxcSegment)).not.toEqual(-1, "No Mxc segment"); diff --git a/spec/unit/BridgedClient.spec.js b/spec/unit/BridgedClient.spec.js index 0c1738ed5..6e7076200 100644 --- a/spec/unit/BridgedClient.spec.js +++ b/spec/unit/BridgedClient.spec.js @@ -31,8 +31,8 @@ describe("BridgedClient", function() { expect(BridgedClient.getValidNick("f+/\u3052oobar", false, STATE_DISC)).toBe("foobar"); }); it("will ensure nicks start with a letter or special character", function() { - expect(BridgedClient.getValidNick("-foobar", false, STATE_DISC)).toBe("M-foobar"); - expect(BridgedClient.getValidNick("12345", false, STATE_DISC)).toBe("M12345"); + expect(BridgedClient.getValidNick("-foobar", false, STATE_DISC)).toBe("`-foobar"); + expect(BridgedClient.getValidNick("12345", false, STATE_DISC)).toBe("`12345"); }); it("will throw if the nick is invalid", function() { expect(() => BridgedClient.getValidNick("f+/\u3052oobar", true, STATE_DISC)).toThrowError(); diff --git a/src/bridge/AdminRoomHandler.ts b/src/bridge/AdminRoomHandler.ts index ad25697b4..82c3ecc13 100644 --- a/src/bridge/AdminRoomHandler.ts +++ b/src/bridge/AdminRoomHandler.ts @@ -89,12 +89,12 @@ const COMMANDS: {[command: string]: Command|Heading} = { 'Actions': { heading: true }, "cmd": { example: `!cmd [irc.example.net] COMMAND [arg0 [arg1 [...]]]`, - summary: "Issue a raw IRC command. These will not produce a reply." + + summary: "Issue a raw IRC command. These will not produce a reply. " + "(Note that the command must be all uppercase.)", }, "feature": { example: `!feature feature-name [true/false/default]`, - summary: `Enable, disable or default a feature's status for your account.` + + summary: `Enable, disable or default a feature's status for your account. ` + `Will display the current feature status if true/false/default not given.`, }, "join": { @@ -428,7 +428,7 @@ export class AdminRoomHandler { const keyword = args[0]; // keyword could be a failed server or a malformed command - if (!keyword.match(/^[A-Z]+$/)) { + if (!keyword.match(/^[A-Za-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}`); diff --git a/src/bridge/IrcBridge.ts b/src/bridge/IrcBridge.ts index 365711f43..d75b4d7af 100644 --- a/src/bridge/IrcBridge.ts +++ b/src/bridge/IrcBridge.ts @@ -1211,18 +1211,20 @@ export class IrcBridge { else if (event.content.membership === "join") { await this.matrixHandler.onJoin(request, memberEvent as unknown as OnMemberEventData, target); } - else if (["ban", "leave"].includes(event.content.membership as string)) { - // Given a "self-kick" is a leave, and you can't ban yourself, - // if the 2 IDs are different then we know it is either a kick - // or a ban (or a rescinded invite) - const isKickOrBan = target.getId() !== sender.getId(); - if (isKickOrBan) { + else if (event.content.membership === "leave") { + // Given a "self-kick" is a leave, if the 2 IDs are different then + // we know it is a kick (or a rescinded invite) + const isKick = target.getId() !== sender.getId(); + if (isKick) { await this.matrixHandler.onKick(request, memberEvent as unknown as MatrixEventKick, sender, target); } else { await this.matrixHandler.onLeave(request, memberEvent, target); } } + else if (event.content.membership === "ban") { + await this.matrixHandler.onBan(request, memberEvent as unknown as MatrixEventKick, sender, target); + } } else if (event.type === "m.room.power_levels" && event.state_key === "") { this.ircHandler.roomAccessSyncer.onMatrixPowerlevelEvent(event); diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts index 89a0885b7..2d7d680bd 100644 --- a/src/bridge/MatrixHandler.ts +++ b/src/bridge/MatrixHandler.ts @@ -55,6 +55,8 @@ export interface MatrixHandlerConfig { shortReplyTemplate: string; // Format of replies sent a while after the original message longReplyTemplate: string; + // format of replies where the sender of the original message is the same as the sender of the reply + selfReplyTemplate: string; // Format of the text explaining why a message is truncated and pastebinned truncatedMessageTemplate: string; // Ignore io.element.functional_members members joining admin rooms. @@ -67,7 +69,8 @@ export const DEFAULTS: MatrixHandlerConfig = { replySourceMaxLength: 32, shortReplyTresholdSeconds: 5 * 60, shortReplyTemplate: "$NICK: $REPLY", - longReplyTemplate: "<$NICK> \"$ORIGINAL\" <- $REPLY", + longReplyTemplate: "$NICK: \"$ORIGINAL\" <- $REPLY", + selfReplyTemplate: "<$NICK> $ORIGINAL\n$REPLY", truncatedMessageTemplate: "(full message at <$URL>)", ignoreFunctionalMembersInAdminRooms: false, }; @@ -652,7 +655,7 @@ export class MatrixHandler { private async _onKick(req: BridgeRequest, event: MatrixEventKick, kicker: MatrixUser, kickee: MatrixUser) { req.log.info( - "onKick %s is kicking/banning %s from %s (reason: %s)", + "onKick %s is kicking %s from %s (reason: %s)", kicker.getId(), kickee.getId(), event.room_id, event.content.reason || "none" ); this._onMemberEvent(req, event); @@ -741,7 +744,81 @@ export class MatrixHandler { // If we aren't joined this will no-op. await client.leaveChannel( ircRoom.channel, - `Kicked by ${kicker.getId()} ` + + `Kicked by ${kicker.getId()}` + + (event.content.reason ? ` : ${event.content.reason}` : "") + ); + }))); + } + } + + private async _onBan(req: BridgeRequest, event: MatrixEventKick, sender: MatrixUser, banned: MatrixUser) { + req.log.info( + "onBan %s is banning %s from %s (reason: %s)", + sender.getId(), banned.getId(), event.room_id, event.content.reason || "none" + ); + this._onMemberEvent(req, event); + + const ircRooms = await this.ircBridge.getStore().getIrcChannelsForRoomId(event.room_id); + // do we have an active connection for the banned? This tells us if they are real + // or virtual. + const bannedClients = this.ircBridge.getBridgedClientsForUserId(banned.getId()); + + if (bannedClients.length === 0) { + // Matrix on IRC banning, work out which IRC user to ban. + let server = null; + for (let i = 0; i < ircRooms.length; i++) { + if (ircRooms[i].server.claimsUserId(banned.getId())) { + server = ircRooms[i].server; + break; + } + } + if (!server) { + return; // kicking a bogus user + } + const bannedNick = server.getNickFromUserId(banned.getId()); + if (!bannedNick) { + return; // bogus virtual user ID + } + // work out which client will do the kicking + const senderClient = this.ircBridge.getIrcUserFromCache(server, sender.getId()); + if (!senderClient) { + // well this is awkward.. whine about it and bail. + req.log.warn( + "%s has no client instance to send kick from. Cannot kick.", + sender.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; + } + senderClient.ban(bannedNick, ircRooms[i].channel); + senderClient.kick( + bannedNick, ircRooms[i].channel, + `Banned by ${sender.getId()}` + + (event.content.reason ? ` : ${event.content.reason}` : "") + ); + } + } + else { + // Matrix on Matrix banning: part the channel. + const bannedServerLookup: {[serverDomain: string]: BridgedClient} = {}; + bannedClients.forEach((ircClient) => { + bannedServerLookup[ircClient.server.domain] = ircClient; + }); + await Promise.all(ircRooms.map((async (ircRoom) => { + // Make the connected IRC client leave the channel. + const client = bannedServerLookup[ircRoom.server.domain]; + if (!client) { + return; // not connected to this server + } + // If we aren't joined this will no-op. + await client.leaveChannel( + ircRoom.channel, + `Banned by ${sender.getId()}` + (event.content.reason ? ` : ${event.content.reason}` : "") ); }))); @@ -1175,10 +1252,9 @@ export class MatrixHandler { // we check event.content.body since ircAction already has the markers stripped const codeBlockMatch = event.content.body.match(/^```(\w+)?/); if (codeBlockMatch) { - const type = codeBlockMatch[1] ? ` ${codeBlockMatch[1]}` : ''; event.content = { - msgtype: "m.emote", - body: `sent a${type} code block: ${httpUrl}` + ...event.content, + body: `${httpUrl}` }; } else { @@ -1209,7 +1285,7 @@ export class MatrixHandler { // Modify the event to become a truncated version of the original // the truncation limits the number of lines sent to lineLimit. - const msg = '\n...(truncated)'; + const msg = '\n(truncated)'; const sendingEvent: MatrixMessageEvent = { ...event, content: { @@ -1299,7 +1375,7 @@ export class MatrixHandler { const bridgeIntent = this.ircBridge.getAppServiceBridge().getIntent(); // strips out the quotation of the original message, if needed const replyText = (body: string): string => { - const REPLY_REGEX = /> <(.*?)>(.*?)\n\n([\s\S]*)/; + const REPLY_REGEX = /> <(.*?)>(.*?)\n\n([\s\S]*)/s; const match = REPLY_REGEX.exec(body); if (match === null || match.length !== 4) { return body; @@ -1394,7 +1470,11 @@ export class MatrixHandler { let replyTemplate: string; const thresholdMs = (this.config.shortReplyTresholdSeconds) * 1000; - if (rplSource && event.origin_server_ts - cachedEvent.timestamp > thresholdMs) { + if (cachedEvent.sender === event.sender) { + // They're replying to their own message. + replyTemplate = this.config.selfReplyTemplate; + } + else if (rplSource && event.origin_server_ts - cachedEvent.timestamp > thresholdMs) { replyTemplate = this.config.longReplyTemplate; } else { @@ -1466,6 +1546,10 @@ export class MatrixHandler { return reqHandler(req, this._onKick(req, event, kicker, kickee)); } + public onBan(req: BridgeRequest, event: MatrixEventKick, sender: MatrixUser, banned: MatrixUser) { + return reqHandler(req, this._onBan(req, event, sender, banned)); + } + public onMessage(req: BridgeRequest, event: MatrixMessageEvent) { return reqHandler(req, this._onMessage(req, event)); } diff --git a/src/irc/BridgedClient.ts b/src/irc/BridgedClient.ts index 932bceb47..8798fdeac 100644 --- a/src/irc/BridgedClient.ts +++ b/src/irc/BridgedClient.ts @@ -510,6 +510,26 @@ export class BridgedClient extends EventEmitter { await c.send("KICK", channel, nick, reason); } + public async ban(nick: string, channel: string): Promise { + if (this.state.status !== BridgedClientStatus.CONNECTED) { + return; // we were never connected to the network. + } + if (!this.state.client.chans.has(channel)) { + // we were never joined to it. We need to be joined to it to kick people. + return; + } + if (!channel.startsWith("#")) { + return; // PM room + } + + const c = this.state.client; + + this.log.debug("Banning %s from channel %s", nick, channel); + + // best effort ban + await c.send("MODE", channel, "+b", nick + "!*@*"); + } + public sendAction(room: IrcRoom, action: IrcAction) { this.keepAlive(); let expiryTs = 0; @@ -738,9 +758,9 @@ export class BridgedClient extends EventEmitter { `Nick '${nick}' must start with a letter or special character (dash is not a special character).` ); } - // Add arbitrary letter prefix. This is important for guest user + // Add arbitrary prefix. This is important for guest user // IDs which are all numbers. - n = "M" + n; + n = "`" + n; } if (state.status === BridgedClientStatus.CONNECTED) { diff --git a/src/models/IrcAction.ts b/src/models/IrcAction.ts index 3768eb5e8..d4e3749c9 100644 --- a/src/models/IrcAction.ts +++ b/src/models/IrcAction.ts @@ -54,18 +54,18 @@ export class IrcAction { return new IrcAction(matrixAction.type, matrixAction.text, matrixAction.ts); case "image": return new IrcAction( - "emote", "uploaded an image: " + matrixAction.text, matrixAction.ts + "message", "" + matrixAction.text, matrixAction.ts ); case "video": return new IrcAction( - "emote", "uploaded a video: " + matrixAction.text, matrixAction.ts + "message", "" + matrixAction.text, matrixAction.ts ); case "audio": return new IrcAction( - "emote", "uploaded an audio file: " + matrixAction.text, matrixAction.ts + "message", "" + matrixAction.text, matrixAction.ts ); case "file": - return new IrcAction("emote", "posted a file: " + matrixAction.text, matrixAction.ts); + return new IrcAction("message", "" + matrixAction.text, matrixAction.ts); case "topic": if (matrixAction.text === null) { break; diff --git a/src/models/MatrixAction.ts b/src/models/MatrixAction.ts index e213aeb46..62d2fbd98 100644 --- a/src/models/MatrixAction.ts +++ b/src/models/MatrixAction.ts @@ -220,12 +220,12 @@ export class MatrixAction { if (filename) { url += `/${encodeURIComponent(filename)}`; - text = `${fileSize} < ${url} >`; + text = `${url} ${fileSize}`; } else { fileSize = fileSize ? ` ${fileSize}` : ""; // If not a filename, print the body - text = `${event.content.body}${fileSize} < ${url} >`; + text = `${url} ${event.content.body}${fileSize}`; } } }