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

Bridge from discord to matrix #778

Open
wants to merge 2 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
30 changes: 15 additions & 15 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class DiscordBot {
this.mxEventProcessor = new MatrixEventProcessor(
new MatrixEventProcessorOpts(config, bridge, this, store),
);
this.discordCommandHandler = new DiscordCommandHandler(bridge, this);
this.discordCommandHandler = new DiscordCommandHandler(bridge, this, store.roomStore, config);
// init vars
this.sentMessages = [];
this.discordMessageQueue = {};
Expand Down Expand Up @@ -188,7 +188,7 @@ export class DiscordBot {
}

public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.PartialUser | Discord.User,
webhookID: string|null = null): Intent {
webhookID: string|null = null): Intent {
if (webhookID) {
// webhookID and user IDs are the same, they are unique, so no need to prefix _webhook_
const name = member instanceof Discord.GuildMember ? member.user.username : member.username;
Expand Down Expand Up @@ -606,8 +606,8 @@ export class DiscordBot {
});
}
} catch (err) {
// throw wrapError(err, Unstable.ForeignNetworkError, "Unable to create \"_matrix\" webhook");
log.warn("Unable to create _matrix webook:", err);
// throw wrapError(err, Unstable.ForeignNetworkError, "Unable to create \"_matrix\" webhook");
log.warn("Unable to create _matrix webook:", err);
}
}
try {
Expand Down Expand Up @@ -751,7 +751,7 @@ export class DiscordBot {
}

public async GetRoomIdsFromGuild(
guild: Discord.Guild, member?: Discord.GuildMember, useCache: boolean = true): Promise<string[]> {
guild: Discord.Guild, member?: Discord.GuildMember, useCache: boolean = true): Promise<string[]> {
if (useCache) {
const res = this.roomIdsForGuildCache.get(`${guild.id}:${member ? member.id : ""}`);

Expand Down Expand Up @@ -825,7 +825,7 @@ export class DiscordBot {
allow: ["SEND_MESSAGES", "VIEW_CHANNEL"],
id: kickee.id,
}],
`Unbanned.`,
`Unbanned.`,
);
this.channelLock.set(botChannel.id);
res = await botChannel.send(
Expand All @@ -843,8 +843,8 @@ export class DiscordBot {
const word = `${kickban === "ban" ? "banned" : "kicked"}`;
this.channelLock.set(botChannel.id);
res = await botChannel.send(
`${kickee} was ${word} from this channel by ${kicker}.`
+ (reason ? ` Reason: ${reason}` : ""),
`${kickee} was ${word} from this channel by ${kicker}.${
reason ? ` Reason: ${reason}` : ""}`,
) as Discord.Message;
this.sentMessages.push(res.id);
this.channelLock.release(botChannel.id);
Expand All @@ -855,7 +855,7 @@ export class DiscordBot {
deny: ["SEND_MESSAGES", "VIEW_CHANNEL"],
id: kickee.id,
}],
`Matrix user was ${word} by ${kicker}.`,
`Matrix user was ${word} by ${kicker}.`,
);
if (kickban === "leave") {
// Kicks will let the user back in after ~30 seconds.
Expand All @@ -866,7 +866,7 @@ export class DiscordBot {
allow: ["SEND_MESSAGES", "VIEW_CHANNEL"],
id: kickee.id,
}],
`Lifting kick since duration expired.`,
`Lifting kick since duration expired.`,
);
}, this.config.room.kickFor);
}
Expand All @@ -885,7 +885,7 @@ export class DiscordBot {
let addText = "";
if (embedSet.replyEmbed) {
for (const line of embedSet.replyEmbed.description!.split("\n")) {
addText += "\n> " + line;
addText += `\n> ${ line}`;
}
}
return embed.description += addText;
Expand Down Expand Up @@ -938,8 +938,8 @@ export class DiscordBot {
}

private async SendMatrixMessage(matrixMsg: IDiscordMessageParserResult, chan: Discord.Channel,
guild: Discord.Guild, author: Discord.User,
msgID: string): Promise<boolean> {
guild: Discord.Guild, author: Discord.User,
msgID: string): Promise<boolean> {
const rooms = await this.channelSync.GetRoomIdsFromChannel(chan);
const intent = this.GetIntentFromDiscordMember(author);

Expand Down Expand Up @@ -1008,11 +1008,11 @@ export class DiscordBot {
// Test for webhooks
if (msg.webhookID) {
const webhook = (await chan.fetchWebhooks())
.filter((h) => h.name === "_matrix").first();
.filter((h) => h.name === "_matrix").first();
if (webhook && msg.webhookID === webhook.id) {
// Filter out our own webhook messages.
log.verbose("Not reflecting own webhook messages");
// Filter out our own webhook messages.
// Filter out our own webhook messages.
MetricPeg.get.requestOutcome(msg.id, true, "dropped");
return;
}
Expand Down
58 changes: 58 additions & 0 deletions src/discordcommandhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ import * as Discord from "better-discord.js";
import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util";
import { Log } from "./log";
import { Appservice } from "matrix-bot-sdk";
import { DbRoomStore } from "./db/roomstore";
import { DiscordBridgeConfig } from "./config";

const log = new Log("DiscordCommandHandler");

export class DiscordCommandHandler {
constructor(
private bridge: Appservice,
private discord: DiscordBot,
private roomStore: DbRoomStore,
private config:DiscordBridgeConfig
) { }

public async Process(msg: Discord.Message) {
Expand Down Expand Up @@ -94,6 +98,12 @@ export class DiscordCommandHandler {
permission: ["MANAGE_WEBHOOKS", "MANAGE_CHANNELS"],
run: async () => this.UnbridgeChannel(chan),
},
bridge: {
description:"Bridges this room to a Matrix channel",
params:["roomid"],
permission:["MANAGE_WEBHOOKS", "MANAGE_CHANNELS"],
run: async ({roomid}) => this.BridgeChannel(roomid,chan),
}
};

const parameters: ICommandParameters = {
Expand All @@ -105,6 +115,12 @@ export class DiscordCommandHandler {
return mxUserId;
},
},
roomid: {
description:"The roomid of matrix room",
get: async (roomid) => {
return roomid;
}
}
};

const permissionCheck: CommandPermissonCheck = async (permission: string|string[]) => {
Expand Down Expand Up @@ -164,4 +180,46 @@ export class DiscordCommandHandler {
"Please try again later or contact the bridge operator.";
}
}

private async BridgeChannel(roomid:string,channel: Discord.TextChannel): Promise<string> {
try {
const roomRes = await this.roomStore.getEntriesByRemoteRoomData({
discord_channel: channel.id,
discord_guild: channel.guild.id,
plumbed: true,
});
if(!roomid){
return ""
}
if(roomRes.length > 0){
return "This guild has already been bridged to a matrix room";
}
if (await this.discord.Provisioner.RoomCountLimitReached(this.config.limits.roomCount)) {
log.info(`Room count limit (value: ${this.config.limits.roomCount}) reached: Rejecting command to bridge new matrix room ${roomid} to ${channel.guild.id}/${channel.id}`);
return `This bridge has reached its room limit of ${this.config.limits.roomCount}. Unbridge another room to allow for new connections.`;
}
try {

log.info(`Bridging discord room ${channel.guild.id}/${channel.id} to ${roomid}`);
await channel.send(
"I'm asking permission from the channel administrators to make this bridge."
);

await this.discord.Provisioner.AskMatrixPermission(this.bridge,channel,roomid);
await this.discord.Provisioner.BridgeMatrixRoom(channel, roomid);
return "I have bridged this room to your channel";
} catch (err) {
if (err.message === "Timed out waiting for a response from the Matrix owners."
|| err.message === "The bridge has been declined by the matrix channel.") {
return err.message;
}

log.error(`Error bridging ${roomid} to ${channel.guild.id}/${channel.id}`);
log.error(err);
return "There was a problem bridging that channel - has the guild owner approved the bridge?";
}
} catch (err) {
return ""
}
}
}
36 changes: 36 additions & 0 deletions src/matrixcommandhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,42 @@ export class MatrixCommandHandler {
}
},
},
approve: {
description: "Deny a pending bridge request",
params: [],
permission: {
cat: "events",
level: PROVISIONING_DEFAULT_POWER_LEVEL,
selfService: true,
subcat: "m.room.power_levels",
},
run: async () => {
if (await this.discord.Provisioner.MarkApprovedFromMatrix(event.room_id, true)) {
return "Thanks for your response! The matrix bridge has been approved.";
} else {
return "Thanks for your response, however" +
" it has arrived after the deadline - sorry!";
}
},
},
deny: {
description: "Deny a pending bridge request",
params: [],
permission: {
cat: "events",
level: PROVISIONING_DEFAULT_POWER_LEVEL,
selfService: true,
subcat: "m.room.power_levels",
},
run: async () => {
if (await this.discord.Provisioner.MarkApprovedFromMatrix(event.room_id, false)) {
return "Thanks for your response! The matrix bridge has been declined.";
} else {
return "Thanks for your response, however" +
" it has arrived after the deadline - sorry!";
}
},
},
};

/*
Expand Down
62 changes: 61 additions & 1 deletion src/provisioner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as Discord from "better-discord.js";
import { DbRoomStore, RemoteStoreRoom, MatrixStoreRoom } from "./db/roomstore";
import { ChannelSyncroniser } from "./channelsyncroniser";
import { Log } from "./log";
import { Appservice } from "matrix-bot-sdk";

const PERMISSION_REQUEST_TIMEOUT = 300000; // 5 minutes

Expand All @@ -26,7 +27,7 @@ const log = new Log("Provisioner");
export class Provisioner {

private pendingRequests: Map<string, (approved: boolean) => void> = new Map(); // [channelId]: resolver fn

private matrixPendingRequests:Map<string, string> = new Map();
constructor(private roomStore: DbRoomStore, private channelSync: ChannelSyncroniser) { }

public async BridgeMatrixRoom(channel: Discord.TextChannel, roomId: string) {
Expand Down Expand Up @@ -114,6 +115,50 @@ export class Provisioner {

}

public async AskMatrixPermission(
bridge: Appservice,
channel: Discord.TextChannel,
roomid:string,
timeout: number = PERMISSION_REQUEST_TIMEOUT): Promise<string> {
const channelId = `${channel.guild.id}/${channel.id}`;

let responded = false;
let resolve: (msg: string) => void;
let reject: (err: Error) => void;
const deferP: Promise<string> = new Promise((res, rej) => {resolve = res; reject = rej; });

const approveFn = (approved: boolean, expired = false) => {
if (responded) {
return;
}

responded = true;
this.pendingRequests.delete(channelId);
this.matrixPendingRequests.delete(roomid);
if (approved) {
resolve("Approved");
} else {
if (expired) {
reject(Error("Timed out waiting for a response from the Matrix owners."));
} else {
reject(Error("The bridge has been declined by the Matrix room."));
}
}
};

this.pendingRequests.set(channelId, approveFn);
this.matrixPendingRequests.set(roomid,channelId);
setTimeout(() => approveFn(false, true), timeout);
await bridge.botIntent.sendText(
roomid,
`${channel.client.user?.username} on discord server ${channel.guild.name} would like to bridge this channel. Someone with permission` +
" to manage webhooks please reply with `!discord approve` or `!discord deny` in the next 5 minutes.",
"m.notice",
);
return await deferP;

}

public HasPendingRequest(channel: Discord.TextChannel): boolean {
const channelId = `${channel.guild.id}/${channel.id}`;
return this.pendingRequests.has(channelId);
Expand All @@ -138,4 +183,19 @@ export class Provisioner {
this.pendingRequests.get(channelId)!(allow);
return true; // replied, so true
}

public async MarkApprovedFromMatrix(
roomid: string,
allow: boolean,
): Promise<boolean> {
if (!this.matrixPendingRequests.has(roomid)) {
return false; // no change, so false
}
const channelId = this.matrixPendingRequests.get(roomid);
if(!channelId){
return false;
}
this.pendingRequests.get(channelId)!(allow);
return true; // replied, so true
}
}