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

Support for ban syncing #1821

Open
wants to merge 3 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
1 change: 1 addition & 0 deletions changelog.d/1821.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Initial support for bridging bans to IRC.
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
14 changes: 8 additions & 6 deletions src/bridge/IrcBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1254,18 +1254,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);
Expand Down
82 changes: 80 additions & 2 deletions src/bridge/MatrixHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,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);
Expand Down Expand Up @@ -741,7 +741,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}` : "")
);
})));
Expand Down Expand Up @@ -1464,6 +1538,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));
}
Expand Down
20 changes: 20 additions & 0 deletions src/irc/BridgedClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,26 @@ export class BridgedClient extends EventEmitter {
await c.send("KICK", channel, nick, reason);
}

public async ban(nick: string, channel: string): Promise<void> {
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;
Expand Down
Loading