Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for ban syncing + minor improvements and bugfixes #1807

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/1807.misc
Original file line number Diff line number Diff line change
@@ -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
6 changes: 4 additions & 2 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <server> <nick> commands to the IRC AS bot.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ properties:
type: "string"
shortReplyTresholdSeconds:
type: "integer"
selfReplyTemplate:
type: "string"
ignoreFunctionalMembersInAdminRooms:
type: "boolean"
ircHandler:
Expand Down
6 changes: 2 additions & 4 deletions spec/integ/admin-rooms.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions spec/integ/kicking.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
80 changes: 61 additions & 19 deletions spec/integ/matrix-to-irc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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('<M-friend> "Message #2" <- Message #3');
expect(text).toEqual('M-friend: "Message #2" <- Message #3');
}
);

Expand Down Expand Up @@ -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" +
Expand All @@ -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();
});
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions spec/unit/BridgedClient.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 3 additions & 3 deletions src/bridge/AdminRoomHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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}`);
Expand Down
Loading