diff --git a/src/bot.ts b/src/bot.ts index eb23cf919ca1da4509f977ccf175a62c81f4c7b7..9afd52cb8052d0a26bd48e20c83323c4fe31034c 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -24,592 +24,596 @@ const MIN_PRESENCE_UPDATE_DELAY = 250; // TODO: This is bad. We should be serving the icon from the own homeserver. const MATRIX_ICON_URL = "https://matrix.org/_matrix/media/r0/download/matrix.org/mlxoESwIsTbJrfXyAAogrNxA"; class ChannelLookupResult { - public channel: Discord.TextChannel; - public botUser: boolean; + public channel: Discord.TextChannel; + public botUser: boolean; } export class DiscordBot { - private config: DiscordBridgeConfig; - private clientFactory: DiscordClientFactory; - private store: DiscordStore; - private bot: Discord.Client; - private bridge: Bridge; - private presenceInterval: any; - private sentMessages: string[]; - private msgProcessor: MessageProcessor; - private mxEventProcessor: MatrixEventProcessor; - private presenceHandler: PresenceHandler; - private userSync: UserSyncroniser; - private channelSync: ChannelSyncroniser; - private roomHandler: MatrixRoomHandler; - - /* Handles messages queued up to be sent to discord. */ - private discordMessageQueue: { [channelId: string]: Promise<any> }; - - constructor(config: DiscordBridgeConfig, store: DiscordStore, private provisioner: Provisioner) { - this.config = config; - this.store = store; - this.sentMessages = []; - this.clientFactory = new DiscordClientFactory(store, config.auth); - this.msgProcessor = new MessageProcessor( - new MessageProcessorOpts(this.config.bridge.domain, this), - ); - this.presenceHandler = new PresenceHandler(this); - this.discordMessageQueue = {}; - } - - public setBridge(bridge: Bridge) { - this.bridge = bridge; - this.mxEventProcessor = new MatrixEventProcessor( - new MatrixEventProcessorOpts(this.config, bridge, this), - ); - } - - public setRoomHandler(roomHandler: MatrixRoomHandler) { - this.roomHandler = roomHandler; - } - - get ClientFactory(): DiscordClientFactory { - return this.clientFactory; - } - - get UserSyncroniser(): UserSyncroniser { - return this.userSync; - } - - get ChannelSyncroniser(): ChannelSyncroniser { - return this.channelSync; - } - - public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.User): any { - return this.bridge.getIntentFromLocalpart(`_discord_${member.id}`); - } - - public run(): Promise<void> { - return this.clientFactory.init().then(() => { - return this.clientFactory.getClient(); - }).then((client: any) => { - if (!this.config.bridge.disableTypingNotifications) { - client.on("typingStart", (c, u) => { this.OnTyping(c, u, true); }); - client.on("typingStop", (c, u) => { this.OnTyping(c, u, false); }); - } - if (!this.config.bridge.disablePresence) { - client.on("presenceUpdate", (_, newMember: Discord.GuildMember) => { - this.presenceHandler.EnqueueUser(newMember.user); - }); - } - this.channelSync = new ChannelSyncroniser(this.bridge, this.config, this); - client.on("channelUpdate", (_, newChannel) => { this.channelSync.OnUpdate(newChannel); }); - client.on("channelDelete", (channel) => { this.channelSync.OnDelete(channel); }); - client.on("guildUpdate", (_, newGuild) => { this.channelSync.OnGuildUpdate(newGuild); }); - client.on("guildDelete", (guild) => { this.channelSync.OnGuildDelete(guild); }); - - // Due to messages often arriving before we get a response from the send call, - // messages get delayed from discord. We use Bluebird.delay to handle this. - - client.on("messageDelete", async (msg: Discord.Message) => { - // tslint:disable-next-line:await-promise - await Bluebird.delay(this.config.limits.discordSendDelay); - this.discordMessageQueue[msg.channel.id] = (async () => { - await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); - await this.OnMessage(msg); - })(); - }); - client.on("messageUpdate", async (oldMessage: Discord.Message, newMessage: Discord.Message) => { - // tslint:disable-next-line:await-promise - await Bluebird.delay(this.config.limits.discordSendDelay); - this.discordMessageQueue[newMessage.channel.id] = (async () => { - await (this.discordMessageQueue[newMessage.channel.id] || Promise.resolve()); - await this.OnMessageUpdate(oldMessage, newMessage); - })(); - }); - client.on("message", async (msg: Discord.Message) => { - // tslint:disable-next-line:await-promise - await Bluebird.delay(this.config.limits.discordSendDelay); - this.discordMessageQueue[msg.channel.id] = (async () => { - await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); - await this.OnMessage(msg); - })(); - }); - const jsLog = new Log("discord.js"); - - this.userSync = new UserSyncroniser(this.bridge, this.config, this); - client.on("userUpdate", (_, user) => this.userSync.OnUpdateUser(user)); - client.on("guildMemberAdd", (user) => this.userSync.OnAddGuildMember(user)); - client.on("guildMemberRemove", (user) => this.userSync.OnRemoveGuildMember(user)); - client.on("guildMemberUpdate", (oldUser, newUser) => this.userSync.OnUpdateGuildMember(oldUser, newUser)); - client.on("debug", (msg) => { jsLog.verbose(msg); }); - client.on("error", (msg) => { jsLog.error(msg); }); - client.on("warn", (msg) => { jsLog.warn(msg); }); - log.info("Discord bot client logged in."); - this.bot = client; - - if (!this.config.bridge.disablePresence) { - if (!this.config.bridge.presenceInterval) { - this.config.bridge.presenceInterval = MIN_PRESENCE_UPDATE_DELAY; - } - this.bot.guilds.forEach((guild) => { - guild.members.forEach((member) => { - if (member.id !== this.GetBotId()) { - this.presenceHandler.EnqueueUser(member.user); - } - }); - }); - this.presenceHandler.Start( - Math.max(this.config.bridge.presenceInterval, MIN_PRESENCE_UPDATE_DELAY), + private config: DiscordBridgeConfig; + private clientFactory: DiscordClientFactory; + private store: DiscordStore; + private bot: Discord.Client; + private bridge: Bridge; + private presenceInterval: any; + private sentMessages: string[]; + private msgProcessor: MessageProcessor; + private mxEventProcessor: MatrixEventProcessor; + private presenceHandler: PresenceHandler; + private userSync: UserSyncroniser; + private channelSync: ChannelSyncroniser; + private roomHandler: MatrixRoomHandler; + + /* Handles messages queued up to be sent to discord. */ + private discordMessageQueue: { [channelId: string]: Promise<any> }; + + constructor(config: DiscordBridgeConfig, store: DiscordStore, private provisioner: Provisioner) { + this.config = config; + this.store = store; + this.sentMessages = []; + this.clientFactory = new DiscordClientFactory(store, config.auth); + this.msgProcessor = new MessageProcessor( + new MessageProcessorOpts(this.config.bridge.domain, this), ); - } - }); - } + this.presenceHandler = new PresenceHandler(this); + this.discordMessageQueue = {}; + } - public GetBotId(): string { - return this.bot.user.id; - } + public setBridge(bridge: Bridge) { + this.bridge = bridge; + this.mxEventProcessor = new MatrixEventProcessor( + new MatrixEventProcessorOpts(this.config, bridge, this), + ); + } - public GetGuilds(): Discord.Guild[] { - return this.bot.guilds.array(); - } + public setRoomHandler(roomHandler: MatrixRoomHandler) { + this.roomHandler = roomHandler; + } - public ThirdpartySearchForChannels(guildId: string, channelName: string): any[] { - if (channelName.startsWith("#")) { - channelName = channelName.substr(1); + get ClientFactory(): DiscordClientFactory { + return this.clientFactory; } - if (this.bot.guilds.has(guildId) ) { - const guild = this.bot.guilds.get(guildId); - return guild.channels.filter((channel) => { - return channel.name.toLowerCase() === channelName.toLowerCase(); // Implement searching in the future. - }).map((channel) => { - return { - alias: `#_discord_${guild.id}_${channel.id}:${this.config.bridge.domain}`, - protocol: "discord", - fields: { - guild_id: guild.id, - channel_name: channel.name, - channel_id: channel.id, - }, - }; - }); - } else { - log.info("Tried to do a third party lookup for a channel, but the guild did not exist"); - return []; + + get UserSyncroniser(): UserSyncroniser { + return this.userSync; } - } - - public LookupRoom(server: string, room: string, sender?: string): Promise<ChannelLookupResult> { - const hasSender = sender !== null; - return this.clientFactory.getClient(sender).then((client) => { - const guild = client.guilds.get(server); - if (!guild) { - throw new Error(`Guild "${server}" not found`); - } - const channel = guild.channels.get(room); - if (channel) { - const lookupResult = new ChannelLookupResult(); - lookupResult.channel = channel; - lookupResult.botUser = this.bot.user.id === client.user.id; - return lookupResult; - } - throw new Error(`Channel "${room}" not found`); - }).catch((err) => { - log.verbose("LookupRoom => ", err); - if (hasSender) { - log.verbose(`Couldn't find guild/channel under user account. Falling back.`); - return this.LookupRoom(server, room, null); - } - throw err; - }); - } - - public async ProcessMatrixStateEvent(event: any): Promise<void> { - log.verbose(`Got state event from ${event.room_id} ${event.type}`); - const channel = await this.GetChannelFromRoomId(event.room_id) as Discord.TextChannel; - const msg = this.mxEventProcessor.StateEventToMessage(event, channel); - if (!msg) { - return; - } - let res = await channel.send(msg); - if (!Array.isArray(res)) { - res = [res]; - } - res.forEach((m: Discord.Message) => { - log.verbose("Sent (state msg) ", m); - this.sentMessages.push(m.id); - const evt = new DbEvent(); - evt.MatrixId = event.event_id + ";" + event.room_id; - evt.DiscordId = m.id; - evt.GuildId = channel.guild.id; - evt.ChannelId = channel.id; - return this.store.Insert(evt); - }); - } - - public async ProcessMatrixMsgEvent(event: any, guildId: string, channelId: string): Promise<null> { - const mxClient = this.bridge.getClientFactory().getClientAs(); - log.verbose(`Looking up ${guildId}_${channelId}`); - const result = await this.LookupRoom(guildId, channelId, event.sender); - const chan = result.channel; - const botUser = result.botUser; - let profile = null; - if (result.botUser) { - // We are doing this through webhooks so fetch the user profile. - profile = await mxClient.getStateEvent(event.room_id, "m.room.member", event.sender); - if (profile === null) { - log.warn(`User ${event.sender} has no member state. That's odd.`); - } + + get ChannelSyncroniser(): ChannelSyncroniser { + return this.channelSync; } - const embedSet = await this.mxEventProcessor.EventToEmbed(event, profile, chan); - const embed = embedSet.messageEmbed; - const opts: Discord.MessageOptions = {}; - const file = await this.mxEventProcessor.HandleAttachment(event, mxClient); - if (typeof(file) === "string") { - embed.description += " " + file; - } else { - opts.file = file; + + public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.User): any { + return this.bridge.getIntentFromLocalpart(`_discord_${member.id}`); } - let msg = null; - let hook: Discord.Webhook ; - if (botUser) { - const webhooks = await chan.fetchWebhooks(); - hook = webhooks.filterArray((h) => h.name === "_matrix").pop(); - // Create a new webhook if none already exists - try { - if (!hook) { - hook = await chan.createWebhook("_matrix", MATRIX_ICON_URL, "Matrix Bridge: Allow rich user messages"); - } - } catch (err) { - log.error("Unable to create \"_matrix\" webhook. ", err); - } + public run(): Promise<void> { + return this.clientFactory.init().then(() => { + return this.clientFactory.getClient(); + }).then((client: any) => { + if (!this.config.bridge.disableTypingNotifications) { + client.on("typingStart", (c, u) => { this.OnTyping(c, u, true); }); + client.on("typingStop", (c, u) => { this.OnTyping(c, u, false); }); + } + if (!this.config.bridge.disablePresence) { + client.on("presenceUpdate", (_, newMember: Discord.GuildMember) => { + this.presenceHandler.EnqueueUser(newMember.user); + }); + } + this.channelSync = new ChannelSyncroniser(this.bridge, this.config, this); + client.on("channelUpdate", (_, newChannel) => { this.channelSync.OnUpdate(newChannel); }); + client.on("channelDelete", (channel) => { this.channelSync.OnDelete(channel); }); + client.on("guildUpdate", (_, newGuild) => { this.channelSync.OnGuildUpdate(newGuild); }); + client.on("guildDelete", (guild) => { this.channelSync.OnGuildDelete(guild); }); + + // Due to messages often arriving before we get a response from the send call, + // messages get delayed from discord. We use Bluebird.delay to handle this. + + client.on("messageDelete", async (msg: Discord.Message) => { + // tslint:disable-next-line:await-promise + await Bluebird.delay(this.config.limits.discordSendDelay); + this.discordMessageQueue[msg.channel.id] = (async () => { + await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); + await this.OnMessage(msg); + })(); + }); + client.on("messageUpdate", async (oldMessage: Discord.Message, newMessage: Discord.Message) => { + // tslint:disable-next-line:await-promise + await Bluebird.delay(this.config.limits.discordSendDelay); + this.discordMessageQueue[newMessage.channel.id] = (async () => { + await (this.discordMessageQueue[newMessage.channel.id] || Promise.resolve()); + await this.OnMessageUpdate(oldMessage, newMessage); + })(); + }); + client.on("message", async (msg: Discord.Message) => { + // tslint:disable-next-line:await-promise + await Bluebird.delay(this.config.limits.discordSendDelay); + this.discordMessageQueue[msg.channel.id] = (async () => { + await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); + await this.OnMessage(msg); + })(); + }); + const jsLog = new Log("discord.js"); + + this.userSync = new UserSyncroniser(this.bridge, this.config, this); + client.on("userUpdate", (_, user) => this.userSync.OnUpdateUser(user)); + client.on("guildMemberAdd", (user) => this.userSync.OnAddGuildMember(user)); + client.on("guildMemberRemove", (user) => this.userSync.OnRemoveGuildMember(user)); + client.on("guildMemberUpdate", (oldUser, newUser) => this.userSync.OnUpdateGuildMember(oldUser, newUser)); + client.on("debug", (msg) => { jsLog.verbose(msg); }); + client.on("error", (msg) => { jsLog.error(msg); }); + client.on("warn", (msg) => { jsLog.warn(msg); }); + log.info("Discord bot client logged in."); + this.bot = client; + + if (!this.config.bridge.disablePresence) { + if (!this.config.bridge.presenceInterval) { + this.config.bridge.presenceInterval = MIN_PRESENCE_UPDATE_DELAY; + } + this.bot.guilds.forEach((guild) => { + guild.members.forEach((member) => { + if (member.id !== this.GetBotId()) { + this.presenceHandler.EnqueueUser(member.user); + } + }); + }); + this.presenceHandler.Start( + Math.max(this.config.bridge.presenceInterval, MIN_PRESENCE_UPDATE_DELAY), + ); + } + }); } - try { - if (!botUser) { - opts.embed = embedSet.replyEmbed; - msg = await chan.send(embed.description, opts); - } else if (hook) { - msg = await hook.send(embed.description, { - username: embed.author.name, - avatarURL: embed.author.icon_url, - files: opts.file ? [opts.file] : undefined, - embeds: embedSet.replyEmbed ? [embedSet.replyEmbed] : undefined, - } as any); - } else { - if (embedSet.replyEmbed) { - embed.addField("Replying to", embedSet.replyEmbed.author.name); - embed.addField("Reply text", embedSet.replyEmbed.description); + + public GetBotId(): string { + return this.bot.user.id; + } + + public GetGuilds(): Discord.Guild[] { + return this.bot.guilds.array(); + } + + public ThirdpartySearchForChannels(guildId: string, channelName: string): any[] { + if (channelName.startsWith("#")) { + channelName = channelName.substr(1); + } + if (this.bot.guilds.has(guildId) ) { + const guild = this.bot.guilds.get(guildId); + return guild.channels.filter((channel) => { + return channel.name.toLowerCase() === channelName.toLowerCase(); // Implement searching in the future. + }).map((channel) => { + return { + alias: `#_discord_${guild.id}_${channel.id}:${this.config.bridge.domain}`, + fields: { + channel_id: channel.id, + channel_name: channel.name, + guild_id: guild.id, + }, + protocol: "discord", + }; + }); + } else { + log.info("Tried to do a third party lookup for a channel, but the guild did not exist"); + return []; } - opts.embed = embed; - msg = await chan.send("", opts); - } - } catch (err) { - log.error("Couldn't send message. ", err); } - if (!Array.isArray(msg)) { - msg = [msg]; + + public LookupRoom(server: string, room: string, sender?: string): Promise<ChannelLookupResult> { + const hasSender = sender !== null; + return this.clientFactory.getClient(sender).then((client) => { + const guild = client.guilds.get(server); + if (!guild) { + throw new Error(`Guild "${server}" not found`); + } + const channel = guild.channels.get(room); + if (channel) { + const lookupResult = new ChannelLookupResult(); + lookupResult.channel = channel; + lookupResult.botUser = this.bot.user.id === client.user.id; + return lookupResult; + } + throw new Error(`Channel "${room}" not found`); + }).catch((err) => { + log.verbose("LookupRoom => ", err); + if (hasSender) { + log.verbose(`Couldn't find guild/channel under user account. Falling back.`); + return this.LookupRoom(server, room, null); + } + throw err; + }); } - msg.forEach((m: Discord.Message) => { - log.verbose("Sent ", m); - this.sentMessages.push(m.id); - const evt = new DbEvent(); - evt.MatrixId = event.event_id + ";" + event.room_id; - evt.DiscordId = m.id; - // Webhooks don't send guild info. - evt.GuildId = guildId; - evt.ChannelId = channelId; - return this.store.Insert(evt); - }); - return; - } - - public async ProcessMatrixRedact(event: any) { - if (this.config.bridge.disableDeletionForwarding) { - return; + + public async ProcessMatrixStateEvent(event: any): Promise<void> { + log.verbose(`Got state event from ${event.room_id} ${event.type}`); + const channel = await this.GetChannelFromRoomId(event.room_id) as Discord.TextChannel; + const msg = this.mxEventProcessor.StateEventToMessage(event, channel); + if (!msg) { + return; + } + let res = await channel.send(msg); + if (!Array.isArray(res)) { + res = [res]; + } + res.forEach((m: Discord.Message) => { + log.verbose("Sent (state msg) ", m); + this.sentMessages.push(m.id); + const evt = new DbEvent(); + evt.MatrixId = event.event_id + ";" + event.room_id; + evt.DiscordId = m.id; + evt.GuildId = channel.guild.id; + evt.ChannelId = channel.id; + return this.store.Insert(evt); + }); } - log.info(`Got redact request for ${event.redacts}`); - log.verbose(`Event:`, event); - const storeEvent = await this.store.Get(DbEvent, {matrix_id: event.redacts + ";" + event.room_id}); + public async ProcessMatrixMsgEvent(event: any, guildId: string, channelId: string): Promise<null> { + const mxClient = this.bridge.getClientFactory().getClientAs(); + log.verbose(`Looking up ${guildId}_${channelId}`); + const result = await this.LookupRoom(guildId, channelId, event.sender); + const chan = result.channel; + const botUser = result.botUser; + let profile = null; + if (result.botUser) { + // We are doing this through webhooks so fetch the user profile. + profile = await mxClient.getStateEvent(event.room_id, "m.room.member", event.sender); + if (profile === null) { + log.warn(`User ${event.sender} has no member state. That's odd.`); + } + } + const embedSet = await this.mxEventProcessor.EventToEmbed(event, profile, chan); + const embed = embedSet.messageEmbed; + const opts: Discord.MessageOptions = {}; + const file = await this.mxEventProcessor.HandleAttachment(event, mxClient); + if (typeof(file) === "string") { + embed.description += " " + file; + } else { + opts.file = file; + } - if (!storeEvent.Result) { - log.warn(`Could not redact because the event was not in the store.`); - return; + let msg = null; + let hook: Discord.Webhook ; + if (botUser) { + const webhooks = await chan.fetchWebhooks(); + hook = webhooks.filterArray((h) => h.name === "_matrix").pop(); + // Create a new webhook if none already exists + try { + if (!hook) { + hook = await chan.createWebhook( + "_matrix", + MATRIX_ICON_URL, + "Matrix Bridge: Allow rich user messages"); + } + } catch (err) { + log.error("Unable to create \"_matrix\" webhook. ", err); + } + } + try { + if (!botUser) { + opts.embed = embedSet.replyEmbed; + msg = await chan.send(embed.description, opts); + } else if (hook) { + msg = await hook.send(embed.description, { + avatarURL: embed.author.icon_url, + embeds: embedSet.replyEmbed ? [embedSet.replyEmbed] : undefined, + files: opts.file ? [opts.file] : undefined, + username: embed.author.name, + } as any); + } else { + if (embedSet.replyEmbed) { + embed.addField("Replying to", embedSet.replyEmbed.author.name); + embed.addField("Reply text", embedSet.replyEmbed.description); + } + opts.embed = embed; + msg = await chan.send("", opts); + } + } catch (err) { + log.error("Couldn't send message. ", err); + } + if (!Array.isArray(msg)) { + msg = [msg]; + } + msg.forEach((m: Discord.Message) => { + log.verbose("Sent ", m); + this.sentMessages.push(m.id); + const evt = new DbEvent(); + evt.MatrixId = event.event_id + ";" + event.room_id; + evt.DiscordId = m.id; + // Webhooks don't send guild info. + evt.GuildId = guildId; + evt.ChannelId = channelId; + return this.store.Insert(evt); + }); + return; } - log.info(`Redact event matched ${storeEvent.ResultCount} entries`); - while (storeEvent.Next()) { - log.info(`Deleting discord msg ${storeEvent.DiscordId}`); - const result = await this.LookupRoom(storeEvent.GuildId, storeEvent.ChannelId, event.sender); - const chan = result.channel; - - const msg = await chan.fetchMessage(storeEvent.DiscordId); - try { - await msg.delete(); - log.info(`Deleted message`); - } catch (ex) { - log.warn(`Failed to delete message`, ex); - } + + public async ProcessMatrixRedact(event: any) { + if (this.config.bridge.disableDeletionForwarding) { + return; + } + log.info(`Got redact request for ${event.redacts}`); + log.verbose(`Event:`, event); + + const storeEvent = await this.store.Get(DbEvent, {matrix_id: event.redacts + ";" + event.room_id}); + + if (!storeEvent.Result) { + log.warn(`Could not redact because the event was not in the store.`); + return; + } + log.info(`Redact event matched ${storeEvent.ResultCount} entries`); + while (storeEvent.Next()) { + log.info(`Deleting discord msg ${storeEvent.DiscordId}`); + const result = await this.LookupRoom(storeEvent.GuildId, storeEvent.ChannelId, event.sender); + const chan = result.channel; + + const msg = await chan.fetchMessage(storeEvent.DiscordId); + try { + await msg.delete(); + log.info(`Deleted message`); + } catch (ex) { + log.warn(`Failed to delete message`, ex); + } + } } - } - public OnUserQuery(userId: string): any { - return false; - } + public OnUserQuery(userId: string): any { + return false; + } - public GetDiscordUserOrMember( - userId: Discord.Snowflake, guildId?: Discord.Snowflake, - ): Promise<Discord.User|Discord.GuildMember> { + public GetDiscordUserOrMember( + userId: Discord.Snowflake, guildId?: Discord.Snowflake, + ): Promise<Discord.User|Discord.GuildMember> { try { if (guildId && this.bot.guilds.has(guildId)) { - return this.bot.guilds.get(guildId).fetchMember(userId); + return this.bot.guilds.get(guildId).fetchMember(userId); } return this.bot.fetchUser(userId); } catch (ex) { log.warn(`Could not fetch user data for ${userId} (guild: ${guildId})`); return undefined; } - } - - public GetChannelFromRoomId(roomId: string): Promise<Discord.Channel> { - return this.bridge.getRoomStore().getEntriesByMatrixId( - roomId, - ).then((entries) => { - if (entries.length === 0) { - log.verbose(`Couldn"t find channel for roomId ${roomId}.`); - return Promise.reject("Room(s) not found."); - } - const entry = entries[0]; - const guild = this.bot.guilds.get(entry.remote.get("discord_guild")); - if (guild) { - const channel = this.bot.channels.get(entry.remote.get("discord_channel")); - if (channel) { - return channel; - } - throw Error("Channel given in room entry not found"); - } - throw Error("Guild given in room entry not found"); - }); - } - - public async GetEmoji(name: string, animated: boolean, id: string): Promise<string> { - if (!id.match(/^\d+$/)) { - throw new Error("Non-numerical ID"); - } - const dbEmoji: DbEmoji = await this.store.Get(DbEmoji, {emoji_id: id}); - if (!dbEmoji.Result) { - const url = "https://cdn.discordapp.com/emojis/" + id + (animated ? ".gif" : ".png"); - const intent = this.bridge.getIntent(); - const mxcUrl = (await Util.UploadContentFromUrl(url, intent, name)).mxcUrl; - dbEmoji.EmojiId = id; - dbEmoji.Name = name; - dbEmoji.Animated = animated; - dbEmoji.MxcUrl = mxcUrl; - await this.store.Insert(dbEmoji); - } - return dbEmoji.MxcUrl; - } - - public GetRoomIdsFromGuild(guild: String): Promise<string[]> { - return this.bridge.getRoomStore().getEntriesByRemoteRoomData({ - discord_guild: guild, - }).then((rooms) => { - if (rooms.length === 0) { - log.verbose(`Couldn't find room(s) for guild id:${guild}.`); - return Promise.reject("Room(s) not found."); - } - return rooms.map((room) => room.matrix.getId()); - }); - } - - private async SendMatrixMessage(matrixMsg: MessageProcessorMatrixResult, chan: Discord.Channel, - guild: Discord.Guild, author: Discord.User, - msgID: string): Promise<boolean> { - const rooms = await this.channelSync.GetRoomIdsFromChannel(chan); - const intent = this.GetIntentFromDiscordMember(author); - - rooms.forEach((room) => { - intent.sendMessage(room, { - body: matrixMsg.body, - msgtype: "m.text", - formatted_body: matrixMsg.formattedBody, - format: "org.matrix.custom.html", - }).then((res) => { - const evt = new DbEvent(); - evt.MatrixId = res.event_id + ";" + room; - evt.DiscordId = msgID; - evt.ChannelId = chan.id; - evt.GuildId = guild.id; - return this.store.Insert(evt); - }); - }); - - // Sending was a success - return true; - } - - private OnTyping(channel: Discord.Channel, user: Discord.User, isTyping: boolean) { - this.channelSync.GetRoomIdsFromChannel(channel).then((rooms) => { - const intent = this.GetIntentFromDiscordMember(user); - return Promise.all(rooms.map((room) => { - return intent.sendTyping(room, isTyping); - })); - }).catch((err) => { - log.warn("Failed to send typing indicator.", err); - }); - } - - private async OnMessage(msg: Discord.Message) { - const indexOfMsg = this.sentMessages.indexOf(msg.id); - const chan = msg.channel as Discord.TextChannel; - if (indexOfMsg !== -1) { - log.verbose("Got repeated message, ignoring."); - delete this.sentMessages[indexOfMsg]; - return; // Skip *our* messages } - if (msg.author.id === this.bot.user.id) { - // We don't support double bridging. - return; - } - // Issue #57: Detect webhooks - if (msg.webhookID != null) { - const webhook = (await chan.fetchWebhooks()) - .filterArray((h) => h.name === "_matrix").pop(); - if (webhook != null && msg.webhookID === webhook.id) { - // Filter out our own webhook messages. - return; - } + + public GetChannelFromRoomId(roomId: string): Promise<Discord.Channel> { + return this.bridge.getRoomStore().getEntriesByMatrixId( + roomId, + ).then((entries) => { + if (entries.length === 0) { + log.verbose(`Couldn"t find channel for roomId ${roomId}.`); + return Promise.reject("Room(s) not found."); + } + const entry = entries[0]; + const guild = this.bot.guilds.get(entry.remote.get("discord_guild")); + if (guild) { + const channel = this.bot.channels.get(entry.remote.get("discord_channel")); + if (channel) { + return channel; + } + throw Error("Channel given in room entry not found"); + } + throw Error("Guild given in room entry not found"); + }); } - // Check if there's an ongoing bridge request - if ((msg.content === "!approve" || msg.content === "!deny") && this.provisioner.HasPendingRequest(chan)) { - try { - const isApproved = msg.content === "!approve"; - const successfullyBridged = await this.provisioner.MarkApproved(chan, msg.member, isApproved); - if (successfullyBridged && isApproved) { - msg.channel.sendMessage("Thanks for your response! The matrix bridge has been approved"); - } else if (successfullyBridged && !isApproved) { - msg.channel.sendMessage("Thanks for your response! The matrix bridge has been declined"); - } else { - msg.channel.sendMessage("Thanks for your response, however the time for responses has expired - sorry!"); + public async GetEmoji(name: string, animated: boolean, id: string): Promise<string> { + if (!id.match(/^\d+$/)) { + throw new Error("Non-numerical ID"); } - } catch (err) { - if (err.message === "You do not have permission to manage webhooks in this channel") { - msg.channel.sendMessage(err.message); - } else { - log.error("Error processing room approval"); - log.error(err); + const dbEmoji: DbEmoji = await this.store.Get(DbEmoji, {emoji_id: id}); + if (!dbEmoji.Result) { + const url = "https://cdn.discordapp.com/emojis/" + id + (animated ? ".gif" : ".png"); + const intent = this.bridge.getIntent(); + const mxcUrl = (await Util.UploadContentFromUrl(url, intent, name)).mxcUrl; + dbEmoji.EmojiId = id; + dbEmoji.Name = name; + dbEmoji.Animated = animated; + dbEmoji.MxcUrl = mxcUrl; + await this.store.Insert(dbEmoji); } - } - - return; // stop processing - we're approving/declining the bridge request + return dbEmoji.MxcUrl; } - // check if it is a command to process by the bot itself - if (msg.content.startsWith("!matrix")) { - await this.roomHandler.HandleDiscordCommand(msg); - return; + public GetRoomIdsFromGuild(guild: string): Promise<string[]> { + return this.bridge.getRoomStore().getEntriesByRemoteRoomData({ + discord_guild: guild, + }).then((rooms) => { + if (rooms.length === 0) { + log.verbose(`Couldn't find room(s) for guild id:${guild}.`); + return Promise.reject("Room(s) not found."); + } + return rooms.map((room) => room.matrix.getId()); + }); } - // Update presence because sometimes discord misses people. - return this.userSync.OnUpdateUser(msg.author).then(() => { - return this.channelSync.GetRoomIdsFromChannel(msg.channel).catch((err) => { - log.verbose("No bridged rooms to send message to. Oh well."); - return null; - }); - }).then((rooms) => { - if (rooms === null) { - return null; - } - const intent = this.GetIntentFromDiscordMember(msg.author); - // Check Attachements - msg.attachments.forEach((attachment) => { - Util.UploadContentFromUrl(attachment.url, intent, attachment.filename).then((content) => { - const fileMime = mime.lookup(attachment.filename); - const msgtype = attachment.height ? "m.image" : "m.file"; - const info = { - mimetype: fileMime, - size: attachment.filesize, - w: null, - h: null, - }; - if (msgtype === "m.image") { - info.w = attachment.width; - info.h = attachment.height; - } - rooms.forEach((room) => { + private async SendMatrixMessage(matrixMsg: MessageProcessorMatrixResult, chan: Discord.Channel, + guild: Discord.Guild, author: Discord.User, + msgID: string): Promise<boolean> { + const rooms = await this.channelSync.GetRoomIdsFromChannel(chan); + const intent = this.GetIntentFromDiscordMember(author); + + rooms.forEach((room) => { intent.sendMessage(room, { - body: attachment.filename, - info, - msgtype, - url: content.mxcUrl, - external_url: attachment.url, - }).then((res) => { - const evt = new DbEvent(); - evt.MatrixId = res.event_id + ";" + room; - evt.DiscordId = msg.id; - evt.ChannelId = msg.channel.id; - evt.GuildId = msg.guild.id; - return this.store.Insert(evt); - }); - }); - }); - }); - if (msg.content !== null && msg.content !== "") { - this.msgProcessor.FormatDiscordMessage(msg).then((result) => { - rooms.forEach((room) => { - const trySend = () => intent.sendMessage(room, { - body: result.body, - msgtype: "m.text", - formatted_body: result.formattedBody, + body: matrixMsg.body, format: "org.matrix.custom.html", - }); - const afterSend = (res) => { + formatted_body: matrixMsg.formattedBody, + msgtype: "m.text", + }).then((res) => { const evt = new DbEvent(); evt.MatrixId = res.event_id + ";" + room; - evt.DiscordId = msg.id; - evt.ChannelId = msg.channel.id; - evt.GuildId = msg.guild.id; + evt.DiscordId = msgID; + evt.ChannelId = chan.id; + evt.GuildId = guild.id; return this.store.Insert(evt); - }; - trySend().then(afterSend).catch((e) => { - if (e.errcode !== "M_FORBIDDEN") { - log.error("DiscordBot", "Failed to send message into room.", e); - return; - } - return this.userSync.EnsureJoin(msg.member, room).then(() => trySend()).then(afterSend); - }); }); }); - } - }).catch((err) => { - log.verbose("Failed to send message into room.", err); - }); - } - - private async OnMessageUpdate(oldMsg: Discord.Message, newMsg: Discord.Message) { - // Check if an edit was actually made - if (oldMsg.content === newMsg.content) { - return; + + // Sending was a success + return true; + } + + private OnTyping(channel: Discord.Channel, user: Discord.User, isTyping: boolean) { + this.channelSync.GetRoomIdsFromChannel(channel).then((rooms) => { + const intent = this.GetIntentFromDiscordMember(user); + return Promise.all(rooms.map((room) => { + return intent.sendTyping(room, isTyping); + })); + }).catch((err) => { + log.warn("Failed to send typing indicator.", err); + }); } - // Create a new edit message using the old and new message contents - const editedMsg = await this.msgProcessor.FormatEdit(oldMsg, newMsg); + private async OnMessage(msg: Discord.Message) { + const indexOfMsg = this.sentMessages.indexOf(msg.id); + const chan = msg.channel as Discord.TextChannel; + if (indexOfMsg !== -1) { + log.verbose("Got repeated message, ignoring."); + delete this.sentMessages[indexOfMsg]; + return; // Skip *our* messages + } + if (msg.author.id === this.bot.user.id) { + // We don't support double bridging. + return; + } + // Issue #57: Detect webhooks + if (msg.webhookID != null) { + const webhook = (await chan.fetchWebhooks()) + .filterArray((h) => h.name === "_matrix").pop(); + if (webhook != null && msg.webhookID === webhook.id) { + // Filter out our own webhook messages. + return; + } + } + + // Check if there's an ongoing bridge request + if ((msg.content === "!approve" || msg.content === "!deny") && this.provisioner.HasPendingRequest(chan)) { + try { + const isApproved = msg.content === "!approve"; + const successfullyBridged = await this.provisioner.MarkApproved(chan, msg.member, isApproved); + if (successfullyBridged && isApproved) { + msg.channel.sendMessage("Thanks for your response! The matrix bridge has been approved"); + } else if (successfullyBridged && !isApproved) { + msg.channel.sendMessage("Thanks for your response! The matrix bridge has been declined"); + } else { + msg.channel.sendMessage("Thanks for your response, however" + + "the time for responses has expired - sorry!"); + } + } catch (err) { + if (err.message === "You do not have permission to manage webhooks in this channel") { + msg.channel.sendMessage(err.message); + } else { + log.error("Error processing room approval"); + log.error(err); + } + } + + return; // stop processing - we're approving/declining the bridge request + } + + // check if it is a command to process by the bot itself + if (msg.content.startsWith("!matrix")) { + await this.roomHandler.HandleDiscordCommand(msg); + return; + } - // Send the message to all bridged matrix rooms - if (!await this.SendMatrixMessage(editedMsg, newMsg.channel, newMsg.guild, newMsg.author, newMsg.id)) { - log.error("Unable to announce message edit for msg id:", newMsg.id); + // Update presence because sometimes discord misses people. + return this.userSync.OnUpdateUser(msg.author).then(() => { + return this.channelSync.GetRoomIdsFromChannel(msg.channel).catch((err) => { + log.verbose("No bridged rooms to send message to. Oh well."); + return null; + }); + }).then((rooms) => { + if (rooms === null) { + return null; + } + const intent = this.GetIntentFromDiscordMember(msg.author); + // Check Attachements + msg.attachments.forEach((attachment) => { + Util.UploadContentFromUrl(attachment.url, intent, attachment.filename).then((content) => { + const fileMime = mime.lookup(attachment.filename); + const msgtype = attachment.height ? "m.image" : "m.file"; + const info = { + h: null, + mimetype: fileMime, + size: attachment.filesize, + w: null, + }; + if (msgtype === "m.image") { + info.w = attachment.width; + info.h = attachment.height; + } + rooms.forEach((room) => { + intent.sendMessage(room, { + body: attachment.filename, + external_url: attachment.url, + info, + msgtype, + url: content.mxcUrl, + }).then((res) => { + const evt = new DbEvent(); + evt.MatrixId = res.event_id + ";" + room; + evt.DiscordId = msg.id; + evt.ChannelId = msg.channel.id; + evt.GuildId = msg.guild.id; + return this.store.Insert(evt); + }); + }); + }); + }); + if (msg.content !== null && msg.content !== "") { + this.msgProcessor.FormatDiscordMessage(msg).then((result) => { + rooms.forEach((room) => { + const trySend = () => intent.sendMessage(room, { + body: result.body, + format: "org.matrix.custom.html", + formatted_body: result.formattedBody, + msgtype: "m.text", + }); + const afterSend = (res) => { + const evt = new DbEvent(); + evt.MatrixId = res.event_id + ";" + room; + evt.DiscordId = msg.id; + evt.ChannelId = msg.channel.id; + evt.GuildId = msg.guild.id; + return this.store.Insert(evt); + }; + trySend().then(afterSend).catch((e) => { + if (e.errcode !== "M_FORBIDDEN") { + log.error("DiscordBot", "Failed to send message into room.", e); + return; + } + return this.userSync.EnsureJoin(msg.member, room).then(() => trySend()).then(afterSend); + }); + }); + }); + } + }).catch((err) => { + log.verbose("Failed to send message into room.", err); + }); } - } - - private async DeleteDiscordMessage(msg: Discord.Message) { - log.info(`Got delete event for ${msg.id}`); - const storeEvent = await this.store.Get(DbEvent, {discord_id: msg.id}); - if (!storeEvent.Result) { - log.warn(`Could not redact because the event was not in the store.`); - return; + + private async OnMessageUpdate(oldMsg: Discord.Message, newMsg: Discord.Message) { + // Check if an edit was actually made + if (oldMsg.content === newMsg.content) { + return; + } + + // Create a new edit message using the old and new message contents + const editedMsg = await this.msgProcessor.FormatEdit(oldMsg, newMsg); + + // Send the message to all bridged matrix rooms + if (!await this.SendMatrixMessage(editedMsg, newMsg.channel, newMsg.guild, newMsg.author, newMsg.id)) { + log.error("Unable to announce message edit for msg id:", newMsg.id); + } } - while (storeEvent.Next()) { - log.info(`Deleting discord msg ${storeEvent.DiscordId}`); - const intent = this.GetIntentFromDiscordMember(msg.author); - const matrixIds = storeEvent.MatrixId.split(";"); - await intent.getClient().redactEvent(matrixIds[1], matrixIds[0]); + + private async DeleteDiscordMessage(msg: Discord.Message) { + log.info(`Got delete event for ${msg.id}`); + const storeEvent = await this.store.Get(DbEvent, {discord_id: msg.id}); + if (!storeEvent.Result) { + log.warn(`Could not redact because the event was not in the store.`); + return; + } + while (storeEvent.Next()) { + log.info(`Deleting discord msg ${storeEvent.DiscordId}`); + const intent = this.GetIntentFromDiscordMember(msg.author); + const matrixIds = storeEvent.MatrixId.split(";"); + await intent.getClient().redactEvent(matrixIds[1], matrixIds[0]); + } } - } } diff --git a/src/channelsyncroniser.ts b/src/channelsyncroniser.ts index 84fa42a4b7756f76c2de452a20e742a8e40208a7..a92a9eae1dc797d592e52c0d5cddcc8743f2dfaa 100644 --- a/src/channelsyncroniser.ts +++ b/src/channelsyncroniser.ts @@ -10,18 +10,18 @@ const log = new Log("ChannelSync"); const POWER_LEVEL_MESSAGE_TALK = 50; const DEFAULT_CHANNEL_STATE = { + iconMxcUrl: null, id: null, mxChannels: [], - iconMxcUrl: null, }; const DEFAULT_SINGLECHANNEL_STATE = { + iconId: null, + iconUrl: null, // nullable mxid: null, name: null, // nullable - topic: null, // nullable - iconUrl: null, // nullable - iconId: null, removeIcon: false, + topic: null, // nullable }; export interface ISingleChannelState { @@ -147,8 +147,8 @@ export class ChannelSyncroniser { } const patternMap = { - name: "#" + channel.name, guild: channel.guild.name, + name: "#" + channel.name, }; let name = this.config.channel.namePattern; for (const p of Object.keys(patternMap)) { diff --git a/src/clientfactory.ts b/src/clientfactory.ts index a9e1b8b944aeddf90d18f893e8cfbb4dba0f6b4a..13e6bff5af205bf45c758912d9a85ea9a7b3633b 100644 --- a/src/clientfactory.ts +++ b/src/clientfactory.ts @@ -9,86 +9,86 @@ const log = new Log("ClientFactory"); const READY_TIMEOUT = 5000; export class DiscordClientFactory { - private config: DiscordBridgeConfigAuth; - private store: DiscordStore; - private botClient: any; - private clients: Map<string, any>; - constructor(store: DiscordStore, config?: DiscordBridgeConfigAuth) { - this.config = config; - this.clients = new Map(); - this.store = store; - } - - public async init(): Promise<void> { - if (this.config === undefined) { - return Promise.reject("Client config not supplied."); + private config: DiscordBridgeConfigAuth; + private store: DiscordStore; + private botClient: any; + private clients: Map<string, any>; + constructor(store: DiscordStore, config?: DiscordBridgeConfigAuth) { + this.config = config; + this.clients = new Map(); + this.store = store; } - // We just need to make sure we have a bearer token. - // Create a new Bot client. - this.botClient = Bluebird.promisifyAll(new Client({ - fetchAllMembers: true, - sync: true, - messageCacheLifetime: 5, - })); - return Bluebird.all([ - this.botClient.onAsync("ready").timeout(READY_TIMEOUT, "Bot timed out waiting for ready."), - this.botClient.login(this.config.botToken), - ]).then(() => { return; }).catch((err) => { - log.error("Could not login as the bot user. This is bad!", err); - throw err; - }); - } - - public getDiscordId(token: String): Bluebird<string> { - const client: any = new Client({ - fetchAllMembers: false, - sync: false, - messageCacheLifetime: 5, - }); - return new Bluebird<string>((resolve, reject) => { - client.on("ready", () => { - const id = client.user.id; - client.destroy(); - resolve(id); - }); - client.login(token).catch(reject); - }).timeout(READY_TIMEOUT).catch((err: Error) => { - log.warn("Could not login as a normal user.", err.message); - throw Error("Could not retrieve ID"); - }); - } - public async getClient(userId: string = null): Promise<any> { - if (userId == null) { - return this.botClient; - } - if (this.clients.has(userId)) { - log.verbose("Returning cached user client for", userId); - return this.clients.get(userId); + public async init(): Promise<void> { + if (this.config === undefined) { + return Promise.reject("Client config not supplied."); + } + // We just need to make sure we have a bearer token. + // Create a new Bot client. + this.botClient = Bluebird.promisifyAll(new Client({ + fetchAllMembers: true, + messageCacheLifetime: 5, + sync: true, + })); + return Bluebird.all([ + this.botClient.onAsync("ready").timeout(READY_TIMEOUT, "Bot timed out waiting for ready."), + this.botClient.login(this.config.botToken), + ]).then(() => { return; }).catch((err) => { + log.error("Could not login as the bot user. This is bad!", err); + throw err; + }); } - const discordIds = await this.store.get_user_discord_ids(userId); - if (discordIds.length === 0) { - return Promise.resolve(this.botClient); + + public getDiscordId(token: string): Bluebird<string> { + const client: any = new Client({ + fetchAllMembers: false, + messageCacheLifetime: 5, + sync: false, + }); + return new Bluebird<string>((resolve, reject) => { + client.on("ready", () => { + const id = client.user.id; + client.destroy(); + resolve(id); + }); + client.login(token).catch(reject); + }).timeout(READY_TIMEOUT).catch((err: Error) => { + log.warn("Could not login as a normal user.", err.message); + throw Error("Could not retrieve ID"); + }); } - // TODO: Select a profile based on preference, not the first one. - const token = await this.store.get_token(discordIds[0]); - const client: any = Bluebird.promisifyAll(new Client({ - fetchAllMembers: true, - sync: true, - messageCacheLifetime: 5, - })); - const jsLog = new Log("discord.js-ppt"); - client.on("debug", (msg) => { jsLog.verbose(msg); }); - client.on("error", (msg) => { jsLog.error(msg); }); - client.on("warn", (msg) => { jsLog.warn(msg); }); - try { - await client.login(token); - log.verbose("Logged in. Storing ", userId); - this.clients.set(userId, client); - return client; - } catch (err) { - log.warn(`Could not log ${userId} in. Returning bot user for now.`, err); - return this.botClient; + + public async getClient(userId: string = null): Promise<any> { + if (userId == null) { + return this.botClient; + } + if (this.clients.has(userId)) { + log.verbose("Returning cached user client for", userId); + return this.clients.get(userId); + } + const discordIds = await this.store.get_user_discord_ids(userId); + if (discordIds.length === 0) { + return Promise.resolve(this.botClient); + } + // TODO: Select a profile based on preference, not the first one. + const token = await this.store.get_token(discordIds[0]); + const client: any = Bluebird.promisifyAll(new Client({ + fetchAllMembers: true, + messageCacheLifetime: 5, + sync: true, + })); + const jsLog = new Log("discord.js-ppt"); + client.on("debug", (msg) => { jsLog.verbose(msg); }); + client.on("error", (msg) => { jsLog.error(msg); }); + client.on("warn", (msg) => { jsLog.warn(msg); }); + try { + await client.login(token); + log.verbose("Logged in. Storing ", userId); + this.clients.set(userId, client); + return client; + } catch (err) { + log.warn(`Could not log ${userId} in. Returning bot user for now.`, err); + return this.botClient; + } } - } } diff --git a/src/config.ts b/src/config.ts index abba23fdc26b038de2e4ea309757ed0c2e35527e..e6ec2628cf2673e8fe18e5d10cf15cf7abefa488 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,91 +1,91 @@ /** Type annotations for config/config.schema.yaml */ export class DiscordBridgeConfig { - public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge(); - public auth: DiscordBridgeConfigAuth = new DiscordBridgeConfigAuth(); - public logging: DiscordBridgeConfigLogging = new DiscordBridgeConfigLogging(); - public database: DiscordBridgeConfigDatabase = new DiscordBridgeConfigDatabase(); - public room: DiscordBridgeConfigRoom = new DiscordBridgeConfigRoom(); - public channel: DiscordBridgeConfigChannel = new DiscordBridgeConfigChannel(); - public limits: DiscordBridgeConfigLimits = new DiscordBridgeConfigLimits(); + public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge(); + public auth: DiscordBridgeConfigAuth = new DiscordBridgeConfigAuth(); + public logging: DiscordBridgeConfigLogging = new DiscordBridgeConfigLogging(); + public database: DiscordBridgeConfigDatabase = new DiscordBridgeConfigDatabase(); + public room: DiscordBridgeConfigRoom = new DiscordBridgeConfigRoom(); + public channel: DiscordBridgeConfigChannel = new DiscordBridgeConfigChannel(); + public limits: DiscordBridgeConfigLimits = new DiscordBridgeConfigLimits(); - /** - * Apply a set of keys and values over the default config. - * @param _config Config keys - * @param configLayer Private parameter - */ - public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) { - Object.keys(newConfig).forEach((key) => { - if ( typeof(configLayer[key]) === "object" && - !Array.isArray(configLayer[key])) { - this.ApplyConfig(newConfig[key], this[key]); - return; - } - configLayer[key] = newConfig[key]; - }); - } + /** + * Apply a set of keys and values over the default config. + * @param _config Config keys + * @param configLayer Private parameter + */ + public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) { + Object.keys(newConfig).forEach((key) => { + if ( typeof(configLayer[key]) === "object" && + !Array.isArray(configLayer[key])) { + this.ApplyConfig(newConfig[key], this[key]); + return; + } + configLayer[key] = newConfig[key]; + }); + } } class DiscordBridgeConfigBridge { - public domain: string; - public homeserverUrl: string; - public presenceInterval: number = 500; - public disablePresence: boolean; - public disableTypingNotifications: boolean; - public disableDiscordMentions: boolean; - public disableDeletionForwarding: boolean; - public enableSelfServiceBridging: boolean; - public disableEveryoneMention: boolean = false; - public disableHereMention: boolean = false; + public domain: string; + public homeserverUrl: string; + public presenceInterval: number = 500; + public disablePresence: boolean; + public disableTypingNotifications: boolean; + public disableDiscordMentions: boolean; + public disableDeletionForwarding: boolean; + public enableSelfServiceBridging: boolean; + public disableEveryoneMention: boolean = false; + public disableHereMention: boolean = false; } export class DiscordBridgeConfigDatabase { - public connString: string; - public filename: string; - public userStorePath: string; - public roomStorePath: string; + public connString: string; + public filename: string; + public userStorePath: string; + public roomStorePath: string; } export class DiscordBridgeConfigAuth { - public clientID: string; - public botToken: string; + public clientID: string; + public botToken: string; } export class DiscordBridgeConfigLogging { - public console: string = "info"; - public lineDateFormat: string = "MMM-D HH:mm:ss.SSS"; - public files: LoggingFile[] = []; + public console: string = "info"; + public lineDateFormat: string = "MMM-D HH:mm:ss.SSS"; + public files: LoggingFile[] = []; } class DiscordBridgeConfigRoom { - public defaultVisibility: string; + public defaultVisibility: string; } class DiscordBridgeConfigChannel { - public namePattern: string = "[Discord] :guild :name"; - public deleteOptions = new DiscordBridgeConfigChannelDeleteOptions(); + public namePattern: string = "[Discord] :guild :name"; + public deleteOptions = new DiscordBridgeConfigChannelDeleteOptions(); } class DiscordBridgeConfigChannelDeleteOptions { - public namePrefix: string = null; - public topicPrefix: string = null; - public disableMessaging: boolean = false; - public unsetRoomAlias: boolean = true; - public unlistFromDirectory: boolean = true; - public setInviteOnly: boolean = true; - public ghostsLeave: boolean = true; + public namePrefix: string = null; + public topicPrefix: string = null; + public disableMessaging: boolean = false; + public unsetRoomAlias: boolean = true; + public unlistFromDirectory: boolean = true; + public setInviteOnly: boolean = true; + public ghostsLeave: boolean = true; } class DiscordBridgeConfigLimits { - public roomGhostJoinDelay: number = 6000; - public discordSendDelay: number = 750; + public roomGhostJoinDelay: number = 6000; + public discordSendDelay: number = 750; } export class LoggingFile { - public file: string; - public level: string = "info"; - public maxFiles: string = "14d"; - public maxSize: string|number = "50m"; - public datePattern: string = "YYYY-MM-DD"; - public enabled: string[] = []; - public disabled: string[] = []; + public file: string; + public level: string = "info"; + public maxFiles: string = "14d"; + public maxSize: string|number = "50m"; + public datePattern: string = "YYYY-MM-DD"; + public enabled: string[] = []; + public disabled: string[] = []; } diff --git a/src/db/dbdataemoji.ts b/src/db/dbdataemoji.ts index 41d16c3bd24bb1d3e40d84ea3e1f4a10edfbc622..79a358d68eb6dc22f3d0244e84d413a2da4d83a9 100644 --- a/src/db/dbdataemoji.ts +++ b/src/db/dbdataemoji.ts @@ -36,11 +36,11 @@ export class DbEmoji implements IDbData { INSERT INTO emoji (emoji_id,name,animated,mxc_url,created_at,updated_at) VALUES ($emoji_id,$name,$animated,$mxc_url,$created_at,$updated_at);`, { - emoji_id: this.EmojiId, - name: this.Name, animated: Number(this.Animated), - mxc_url: this.MxcUrl, created_at: this.CreatedAt, + emoji_id: this.EmojiId, + mxc_url: this.MxcUrl, + name: this.Name, updated_at: this.UpdatedAt, }); } @@ -56,10 +56,10 @@ export class DbEmoji implements IDbData { updated_at = $updated_at WHERE emoji_id = $emoji_id`, { - emoji_id: this.EmojiId, - name: this.Name, animated: Number(this.Animated), + emoji_id: this.EmojiId, mxc_url: this.MxcUrl, + name: this.Name, updated_at: this.UpdatedAt, }); } diff --git a/src/db/dbdataevent.ts b/src/db/dbdataevent.ts index 8756b9ec431a2b4ad5dbe9ae1fc9c7030fda57fb..ab66fe023023783447fe193f0c693f99103b80a0 100644 --- a/src/db/dbdataevent.ts +++ b/src/db/dbdataevent.ts @@ -36,8 +36,8 @@ export class DbEvent implements IDbDataMany { for (const rowM of rowsM) { const row = { - matrix_id: rowM.matrix_id, discord_id: rowM.discord_id, + matrix_id: rowM.matrix_id, }; for (const rowD of await store.db.All(` SELECT * @@ -72,8 +72,8 @@ export class DbEvent implements IDbDataMany { INSERT INTO event_store (matrix_id,discord_id) VALUES ($matrix_id,$discord_id);`, { - matrix_id: this.MatrixId, discord_id: this.DiscordId, + matrix_id: this.MatrixId, }); // Check if the discord item exists? const msgExists = await store.db.Get(` @@ -89,9 +89,9 @@ export class DbEvent implements IDbDataMany { INSERT INTO discord_msg_store (msg_id, guild_id, channel_id) VALUES ($msg_id, $guild_id, $channel_id);`, { - msg_id: this.DiscordId, - guild_id: this.GuildId, channel_id: this.ChannelId, + guild_id: this.GuildId, + msg_id: this.DiscordId, }); } @@ -104,8 +104,8 @@ export class DbEvent implements IDbDataMany { DELETE FROM event_store WHERE matrix_id = $matrix_id AND discord_id = $discord_id;`, { - matrix_id: this.MatrixId, discord_id: this.DiscordId, + matrix_id: this.MatrixId, }); return store.db.Run(` DELETE FROM discord_msg_store diff --git a/src/db/postgres.ts b/src/db/postgres.ts index c5a7cde731ca22d27a28c7904fbbd6b71739fd25..b0207bd7b517d1cd2689c56f00ec32569517e274 100644 --- a/src/db/postgres.ts +++ b/src/db/postgres.ts @@ -1,10 +1,9 @@ -import {IMain, IDatabase} from "pg-promise"; import * as pgPromise from "pg-promise"; import { Log } from "../log"; import { IDatabaseConnector } from "./connector"; const log = new Log("SQLite3"); -const pgp: IMain = pgPromise({ +const pgp: pgPromise.IMain = pgPromise({ // Initialization Options }); @@ -15,7 +14,7 @@ export class Postgres implements IDatabaseConnector { }); } - private db: IDatabase<any>; + private db: pgPromise.IDatabase<any>; constructor(private connectionString: string) { } diff --git a/src/db/schema/dbschema.ts b/src/db/schema/dbschema.ts index 563aeae5ae1b0d9032635741fc2dfa897a1f4c5a..e96ceb6c4b28b0c6ebd8cea095ce61a35db157cc 100644 --- a/src/db/schema/dbschema.ts +++ b/src/db/schema/dbschema.ts @@ -1,6 +1,6 @@ import { DiscordStore } from "../../store"; export interface IDbSchema { - description: string; - run(store: DiscordStore): Promise<null|void|Error|Error[]>; - rollBack(store: DiscordStore): Promise<null|void|Error|Error[]>; + description: string; + run(store: DiscordStore): Promise<null|void|Error|Error[]>; + rollBack(store: DiscordStore): Promise<null|void|Error|Error[]>; } diff --git a/src/db/schema/v1.ts b/src/db/schema/v1.ts index c2824a58c41bf9d9add8a57042d2cee61059e741..20004bb01b2e9458cdb2f003d082cc370dc5161c 100644 --- a/src/db/schema/v1.ts +++ b/src/db/schema/v1.ts @@ -1,25 +1,25 @@ import {IDbSchema} from "./dbschema"; import {DiscordStore} from "../../store"; export class Schema implements IDbSchema { - public description = "Schema, Client Auth Table"; - public run(store: DiscordStore): Promise<Error> { - return store.create_table(` - CREATE TABLE schema ( - version INTEGER UNIQUE NOT NULL - );`, "schema").then(() => { - return store.db.Exec("INSERT INTO schema VALUES (0);"); - }).then(() => { - return store.create_table(` - CREATE TABLE user_tokens ( - userId TEXT UNIQUE NOT NULL, - token TEXT UNIQUE NOT NULL - );`, "user_tokens"); - }); - } - public rollBack(store: DiscordStore): Promise<Error> { - return store.db.Exec( - `DROP TABLE IF EXISTS schema; - DROP TABLE IF EXISTS user_tokens`, - ); - } + public description = "Schema, Client Auth Table"; + public run(store: DiscordStore): Promise<Error> { + return store.create_table(` + CREATE TABLE schema ( + version INTEGER UNIQUE NOT NULL + );`, "schema").then(() => { + return store.db.Exec("INSERT INTO schema VALUES (0);"); + }).then(() => { + return store.create_table(` + CREATE TABLE user_tokens ( + userId TEXT UNIQUE NOT NULL, + token TEXT UNIQUE NOT NULL + );`, "user_tokens"); + }); + } + public rollBack(store: DiscordStore): Promise<Error> { + return store.db.Exec( + `DROP TABLE IF EXISTS schema; + DROP TABLE IF EXISTS user_tokens`, + ); + } } diff --git a/src/db/schema/v2.ts b/src/db/schema/v2.ts index 2f6d8a10b8b056bb53f696b55ecb5905b2af5aad..c5c0cf4b6aed614030836cfe6145e442d1046623 100644 --- a/src/db/schema/v2.ts +++ b/src/db/schema/v2.ts @@ -1,26 +1,26 @@ import {IDbSchema} from "./dbschema"; import {DiscordStore} from "../../store"; export class Schema implements IDbSchema { - public description = "Create DM Table, User Options"; - public run(store: DiscordStore): Promise<Error[]> { - return Promise.all([ - store.create_table(` - CREATE TABLE dm_rooms ( - discord_id TEXT NOT NULL, - channel_id TEXT NOT NULL, - room_id TEXT UNIQUE NOT NULL - );`, "dm_rooms"), - store.create_table(` - CREATE TABLE client_options ( - discord_id TEXT UNIQUE NOT NULL, - options INTEGER NOT NULL - );`, "client_options", - )]); - } - public rollBack(store: DiscordStore): Promise<Error> { - return store.db.Exec( - `DROP TABLE IF EXISTS dm_rooms; - DROP TABLE IF EXISTS client_options;`, - ); - } + public description = "Create DM Table, User Options"; + public run(store: DiscordStore): Promise<Error[]> { + return Promise.all([ + store.create_table(` + CREATE TABLE dm_rooms ( + discord_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + room_id TEXT UNIQUE NOT NULL + );`, "dm_rooms"), + store.create_table(` + CREATE TABLE client_options ( + discord_id TEXT UNIQUE NOT NULL, + options INTEGER NOT NULL + );`, "client_options", + )]); + } + public rollBack(store: DiscordStore): Promise<Error> { + return store.db.Exec( + `DROP TABLE IF EXISTS dm_rooms; + DROP TABLE IF EXISTS client_options;`, + ); + } } diff --git a/src/db/schema/v3.ts b/src/db/schema/v3.ts index bc2440603775e425496a3c834732a2a292aa5e25..ab4562785c0ed95b671c19530c7a5cc16badddcb 100644 --- a/src/db/schema/v3.ts +++ b/src/db/schema/v3.ts @@ -6,92 +6,92 @@ import { Log } from "../../log"; const log = new Log("SchemaV3"); export class Schema implements IDbSchema { - public description = "user_tokens split into user_id_discord_id"; - public run(store: DiscordStore): Promise<null> { - const promise = Promise.all([store.create_table(` - CREATE TABLE user_id_discord_id ( - discord_id TEXT NOT NULL, - user_id TEXT NOT NULL, - PRIMARY KEY(discord_id, user_id) - );`, "user_id_discord_id"), - store.create_table(` - CREATE TABLE discord_id_token ( - discord_id TEXT UNIQUE NOT NULL, - token TEXT NOT NULL, - PRIMARY KEY(discord_id) - );`, "discord_id_token", - )]); - return promise.then(() => { - // Backup before moving data. - return store.backup_database(); - }).then(() => { - // Move old data to new tables. - return this.moveUserIds(store); - }).then(() => { - // Drop old table. - return store.db.Run( - `DROP TABLE IF EXISTS user_tokens;`, - ); - }); - } + public description = "user_tokens split into user_id_discord_id"; + public run(store: DiscordStore): Promise<null> { + const promise = Promise.all([store.create_table(` + CREATE TABLE user_id_discord_id ( + discord_id TEXT NOT NULL, + user_id TEXT NOT NULL, + PRIMARY KEY(discord_id, user_id) + );`, "user_id_discord_id"), + store.create_table(` + CREATE TABLE discord_id_token ( + discord_id TEXT UNIQUE NOT NULL, + token TEXT NOT NULL, + PRIMARY KEY(discord_id) + );`, "discord_id_token", + )]); + return promise.then(() => { + // Backup before moving data. + return store.backup_database(); + }).then(() => { + // Move old data to new tables. + return this.moveUserIds(store); + }).then(() => { + // Drop old table. + return store.db.Run( + `DROP TABLE IF EXISTS user_tokens;`, + ); + }); + } - public rollBack(store: DiscordStore): Promise <void> { - return Promise.all([store.db.Run( - `DROP TABLE IF EXISTS user_id_discord_id;`, - ), store.db.Run( - `DROP TABLE IF EXISTS discord_id_token;`, - )]).then(() => { + public rollBack(store: DiscordStore): Promise <void> { + return Promise.all([store.db.Run( + `DROP TABLE IF EXISTS user_id_discord_id;`, + ), store.db.Run( + `DROP TABLE IF EXISTS discord_id_token;`, + )]).then(() => { - }); - } + }); + } - private async moveUserIds(store: DiscordStore): Promise <null> { - log.info("Performing one time moving of tokens to new table. Please wait."); - let rows; - try { - rows = await store.db.All(`SELECT * FROM user_tokens`); - } catch (err) { - log.error(` + private async moveUserIds(store: DiscordStore): Promise <null> { + log.info("Performing one time moving of tokens to new table. Please wait."); + let rows; + try { + rows = await store.db.All(`SELECT * FROM user_tokens`); + } catch (err) { + log.error(` Could not select users from 'user_tokens'.It is possible that the table does not exist on your database in which case you can proceed safely. Otherwise a copy of the database before the schema update has been placed in the root directory.`); - log.error(err); - return; - } - const promises = []; - const clientFactory = new DiscordClientFactory(store); - for (const row of rows) { - log.info("Moving ", row.userId); - try { - const dId = clientFactory.getDiscordId(row.token); - if (dId === null) { - continue; - } - log.verbose("INSERT INTO discord_id_token."); - await store.db.Run( - ` - INSERT INTO discord_id_token (discord_id,token) - VALUES ($discordId,$token); - ` - , { - $discordId: dId, - $token: row.token, - }); - log.verbose("INSERT INTO user_id_discord_id."); - await store.db.Run( - ` - INSERT INTO user_id_discord_id (discord_id,user_id) - VALUES ($discordId,$userId); - ` - , { - $discordId: dId, - $userId: row.userId, - }); - } catch (err) { - log.error(`Couldn't move ${row.userId}'s token into new table.`); - log.error(err); + log.error(err); + return; + } + const promises = []; + const clientFactory = new DiscordClientFactory(store); + for (const row of rows) { + log.info("Moving ", row.userId); + try { + const dId = clientFactory.getDiscordId(row.token); + if (dId === null) { + continue; + } + log.verbose("INSERT INTO discord_id_token."); + await store.db.Run( + ` + INSERT INTO discord_id_token (discord_id,token) + VALUES ($discordId,$token); + ` + , { + $discordId: dId, + $token: row.token, + }); + log.verbose("INSERT INTO user_id_discord_id."); + await store.db.Run( + ` + INSERT INTO user_id_discord_id (discord_id,user_id) + VALUES ($discordId,$userId); + ` + , { + $discordId: dId, + $userId: row.userId, + }); + } catch (err) { + log.error(`Couldn't move ${row.userId}'s token into new table.`); + log.error(err); + } + } } - } -} } diff --git a/src/db/schema/v4.ts b/src/db/schema/v4.ts index 80443ab5458b578f94dc0eb55f6ad4c9feb2c4e9..602583fc03cc5dd204fbf4da1b529aee1f0ed18f 100644 --- a/src/db/schema/v4.ts +++ b/src/db/schema/v4.ts @@ -2,23 +2,23 @@ import {IDbSchema} from "./dbschema"; import {DiscordStore} from "../../store"; export class Schema implements IDbSchema { - public description = "create guild emoji table"; - public run(store: DiscordStore): Promise<Error> { - return store.create_table(` - CREATE TABLE guild_emoji ( - emoji_id TEXT NOT NULL, - guild_id TEXT NOT NULL, - name TEXT NOT NULL, - mxc_url TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - PRIMARY KEY(emoji_id, guild_id) - );`, "guild_emoji"); - } + public description = "create guild emoji table"; + public run(store: DiscordStore): Promise<Error> { + return store.create_table(` + CREATE TABLE guild_emoji ( + emoji_id TEXT NOT NULL, + guild_id TEXT NOT NULL, + name TEXT NOT NULL, + mxc_url TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY(emoji_id, guild_id) + );`, "guild_emoji"); + } - public rollBack(store: DiscordStore): Promise <Error> { - return store.db.Run( - `DROP TABLE IF EXISTS guild_emoji;`, - ); - } + public rollBack(store: DiscordStore): Promise <Error> { + return store.db.Run( + `DROP TABLE IF EXISTS guild_emoji;`, + ); + } } diff --git a/src/db/schema/v5.ts b/src/db/schema/v5.ts index 223bc44637a6e3f8271ce55c056e59d217e9b405..aaa31a9bb5c4bb0f4d0f14b2f6ee1448bc0e91a0 100644 --- a/src/db/schema/v5.ts +++ b/src/db/schema/v5.ts @@ -2,19 +2,19 @@ import {IDbSchema} from "./dbschema"; import {DiscordStore} from "../../store"; export class Schema implements IDbSchema { - public description = "create event_store table"; - public run(store: DiscordStore): Promise<Error> { - return store.create_table(` - CREATE TABLE event_store ( - matrix_id TEXT NOT NULL, - discord_id TEXT NOT NULL, - PRIMARY KEY(matrix_id, discord_id) - );`, "event_store"); - } + public description = "create event_store table"; + public run(store: DiscordStore): Promise<Error> { + return store.create_table(` + CREATE TABLE event_store ( + matrix_id TEXT NOT NULL, + discord_id TEXT NOT NULL, + PRIMARY KEY(matrix_id, discord_id) + );`, "event_store"); + } - public rollBack(store: DiscordStore): Promise <Error> { - return store.db.Run( - `DROP TABLE IF EXISTS event_store;`, - ); - } + public rollBack(store: DiscordStore): Promise <Error> { + return store.db.Run( + `DROP TABLE IF EXISTS event_store;`, + ); + } } diff --git a/src/db/schema/v6.ts b/src/db/schema/v6.ts index 74c7a4c38090c9c0961f13620f979dadad8fb613..29e29e552f50acd8f2a6d1db775bccddcc79b7cf 100644 --- a/src/db/schema/v6.ts +++ b/src/db/schema/v6.ts @@ -2,30 +2,30 @@ import {IDbSchema} from "./dbschema"; import {DiscordStore} from "../../store"; export class Schema implements IDbSchema { - public description = "create event_store and discord_msg_store tables"; - public async run(store: DiscordStore): Promise<void|Error> { - await store.db.Run( - `DROP TABLE IF EXISTS event_store;`, - ); - await store.create_table(` - CREATE TABLE event_store ( - matrix_id TEXT NOT NULL, - discord_id TEXT NOT NULL, - PRIMARY KEY(matrix_id, discord_id) - );`, "event_store"); - return await store.create_table(` - CREATE TABLE discord_msg_store ( - msg_id TEXT NOT NULL, - guild_id TEXT NOT NULL, - channel_id TEXT NOT NULL, - PRIMARY KEY(msg_id) - );`, "discord_msg_store"); - } + public description = "create event_store and discord_msg_store tables"; + public async run(store: DiscordStore): Promise<void|Error> { + await store.db.Run( + `DROP TABLE IF EXISTS event_store;`, + ); + await store.create_table(` + CREATE TABLE event_store ( + matrix_id TEXT NOT NULL, + discord_id TEXT NOT NULL, + PRIMARY KEY(matrix_id, discord_id) + );`, "event_store"); + return await store.create_table(` + CREATE TABLE discord_msg_store ( + msg_id TEXT NOT NULL, + guild_id TEXT NOT NULL, + channel_id TEXT NOT NULL, + PRIMARY KEY(msg_id) + );`, "discord_msg_store"); + } - public rollBack(store: DiscordStore): Promise <null> { - return store.db.Exec( - `DROP TABLE IF EXISTS event_store;` + - `DROP TABLE IF EXISTS discord_msg_store;`, - ); - } + public rollBack(store: DiscordStore): Promise <null> { + return store.db.Exec( + `DROP TABLE IF EXISTS event_store;` + + `DROP TABLE IF EXISTS discord_msg_store;`, + ); + } } diff --git a/src/db/schema/v7.ts b/src/db/schema/v7.ts index adb3a336a58867aad5c0daec3e6d827532acf46a..4fba9edc0ed1eed3d33ac8dbc48418190334e71d 100644 --- a/src/db/schema/v7.ts +++ b/src/db/schema/v7.ts @@ -5,33 +5,33 @@ import { Log } from "../../log"; const log = new Log("SchemaV7"); export class Schema implements IDbSchema { - public description = "create guild emoji table"; - public run(store: DiscordStore): Promise<null> { - return store.create_table(` - CREATE TABLE emoji ( - emoji_id TEXT NOT NULL, - name TEXT NOT NULL, - animated INTEGER NOT NULL, - mxc_url TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - PRIMARY KEY(emoji_id) - );`, "emoji").then(() => { - // migrate existing emoji - return store.db.Run(` - INSERT INTO emoji - (emoji_id, name, animated, mxc_url, created_at, updated_at) - SELECT emoji_id, name, 0 AS animated, mxc_url, created_at, updated_at FROM guild_emoji; - `).catch(() => { - // ignore errors - log.warning("Failed to migrate old data to new table"); - }); - }); - } + public description = "create guild emoji table"; + public run(store: DiscordStore): Promise<null> { + return store.create_table(` + CREATE TABLE emoji ( + emoji_id TEXT NOT NULL, + name TEXT NOT NULL, + animated INTEGER NOT NULL, + mxc_url TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY(emoji_id) + );`, "emoji").then(() => { + // migrate existing emoji + return store.db.Run(` + INSERT INTO emoji + (emoji_id, name, animated, mxc_url, created_at, updated_at) + SELECT emoji_id, name, 0 AS animated, mxc_url, created_at, updated_at FROM guild_emoji; + `).catch(() => { + // ignore errors + log.warning("Failed to migrate old data to new table"); + }); + }); + } - public rollBack(store: DiscordStore): Promise <null> { - return store.db.Run( - `DROP TABLE IF EXISTS emoji;`, - ); - } + public rollBack(store: DiscordStore): Promise <null> { + return store.db.Run( + `DROP TABLE IF EXISTS emoji;`, + ); + } } diff --git a/src/discordas.ts b/src/discordas.ts index 2a09458e4c5c57c157d3715bd629a9c34553eb00..fa8d870e6031985579ef4826654ad6b48c1c03ce 100644 --- a/src/discordas.ts +++ b/src/discordas.ts @@ -12,104 +12,103 @@ import { Log } from "./log"; const log = new Log("DiscordAS"); const cli = new Cli({ - bridgeConfig: { - affectsRegistration: true, - schema: "./config/config.schema.yaml", - }, - registrationPath: "discord-registration.yaml", - generateRegistration, - run, + bridgeConfig: { + affectsRegistration: true, + schema: "./config/config.schema.yaml", + }, + generateRegistration, + registrationPath: "discord-registration.yaml", + run, }); try { - cli.run(); + cli.run(); } catch (err) { - log.error("Failed to start bridge."); - log.error(err); + log.error("Failed to start bridge."); + log.error(err); } function generateRegistration(reg, callback) { - reg.setId(AppServiceRegistration.generateToken()); - reg.setHomeserverToken(AppServiceRegistration.generateToken()); - reg.setAppServiceToken(AppServiceRegistration.generateToken()); - reg.setSenderLocalpart("_discord_bot"); - reg.addRegexPattern("users", "@_discord_.*", true); - reg.addRegexPattern("aliases", "#_discord_.*", true); - reg.setRateLimited(false); - reg.setProtocols(["discord"]); - callback(reg); + reg.setId(AppServiceRegistration.generateToken()); + reg.setHomeserverToken(AppServiceRegistration.generateToken()); + reg.setAppServiceToken(AppServiceRegistration.generateToken()); + reg.setSenderLocalpart("_discord_bot"); + reg.addRegexPattern("users", "@_discord_.*", true); + reg.addRegexPattern("aliases", "#_discord_.*", true); + reg.setRateLimited(false); + reg.setProtocols(["discord"]); + callback(reg); } function run(port: number, fileConfig: DiscordBridgeConfig) { - const config = new DiscordBridgeConfig(); - config.ApplyConfig(fileConfig); - Log.Configure(config.logging); - log.info("Starting Discord AS"); - const yamlConfig = yaml.safeLoad(fs.readFileSync(cli.opts.registrationPath, "utf8")); - const registration = AppServiceRegistration.fromObject(yamlConfig); - if (registration === null) { - throw new Error("Failed to parse registration file"); - } + const config = new DiscordBridgeConfig(); + config.ApplyConfig(fileConfig); + Log.Configure(config.logging); + log.info("Starting Discord AS"); + const yamlConfig = yaml.safeLoad(fs.readFileSync(cli.opts.registrationPath, "utf8")); + const registration = AppServiceRegistration.fromObject(yamlConfig); + if (registration === null) { + throw new Error("Failed to parse registration file"); + } - const botUserId = "@" + registration.sender_localpart + ":" + config.bridge.domain; - const clientFactory = new ClientFactory({ - appServiceUserId: botUserId, - token: registration.as_token, - url: config.bridge.homeserverUrl, - }); - const provisioner = new Provisioner(); - // Warn and deprecate old config options. - const discordstore = new DiscordStore(config.database); - const discordbot = new DiscordBot(config, discordstore, provisioner); - const roomhandler = new MatrixRoomHandler(discordbot, config, botUserId, provisioner); + const botUserId = "@" + registration.sender_localpart + ":" + config.bridge.domain; + const clientFactory = new ClientFactory({ + appServiceUserId: botUserId, + token: registration.as_token, + url: config.bridge.homeserverUrl, + }); + const provisioner = new Provisioner(); + // Warn and deprecate old config options. + const discordstore = new DiscordStore(config.database); + const discordbot = new DiscordBot(config, discordstore, provisioner); + const roomhandler = new MatrixRoomHandler(discordbot, config, botUserId, provisioner); - const bridge = new Bridge({ - clientFactory, - controller: { - // onUserQuery: userQuery, - onAliasQuery: roomhandler.OnAliasQuery.bind(roomhandler), - onEvent: (request, context) => - request.outcomeFrom(Bluebird.resolve(roomhandler.OnEvent(request, context))) - , - onAliasQueried: roomhandler.OnAliasQueried.bind(roomhandler), - thirdPartyLookup: roomhandler.ThirdPartyLookup, - onLog: (line, isError) => { - log.verbose("matrix-appservice-bridge", line); - }, - }, - intentOptions: { - clients: { - dontJoin: true, // handled manually - }, - }, - domain: config.bridge.domain, - homeserverUrl: config.bridge.homeserverUrl, - registration, - userStore: config.database.userStorePath, - roomStore: config.database.roomStorePath, - // To avoid out of order message sending. - queue: { - type: "per_room", - perRequest: true, - }, - }); - provisioner.SetBridge(bridge); - roomhandler.setBridge(bridge); - discordbot.setBridge(bridge); - discordbot.setRoomHandler(roomhandler); - log.info("Initing bridge."); - log.info(`Started listening on port ${port}.`); - bridge.run(port, config).then(() => { - log.info("Initing store."); - return discordstore.init(); - }).then(() => { - log.info("Initing bot."); - return discordbot.run().then(() => { - log.info("Discordbot started successfully."); + const bridge = new Bridge({ + clientFactory, + controller: { + // onUserQuery: userQuery, + onAliasQueried: roomhandler.OnAliasQueried.bind(roomhandler), + onAliasQuery: roomhandler.OnAliasQuery.bind(roomhandler), + onEvent: (request, context) => + request.outcomeFrom(Bluebird.resolve(roomhandler.OnEvent(request, context))), + onLog: (line, isError) => { + log.verbose("matrix-appservice-bridge", line); + }, + thirdPartyLookup: roomhandler.ThirdPartyLookup, + }, + domain: config.bridge.domain, + homeserverUrl: config.bridge.homeserverUrl, + intentOptions: { + clients: { + dontJoin: true, // handled manually + }, + }, + queue: { + perRequest: true, + type: "per_room", + }, + registration, + roomStore: config.database.roomStorePath, + userStore: config.database.userStorePath, + // To avoid out of order message sending. + }); + provisioner.SetBridge(bridge); + roomhandler.setBridge(bridge); + discordbot.setBridge(bridge); + discordbot.setRoomHandler(roomhandler); + log.info("Initing bridge."); + log.info(`Started listening on port ${port}.`); + bridge.run(port, config).then(() => { + log.info("Initing store."); + return discordstore.init(); + }).then(() => { + log.info("Initing bot."); + return discordbot.run().then(() => { + log.info("Discordbot started successfully."); + }); + }).catch((err) => { + log.error(err); + log.error("Failure during startup. Exiting."); + process.exit(1); }); - }).catch((err) => { - log.error(err); - log.error("Failure during startup. Exiting."); - process.exit(1); - }); } diff --git a/src/log.ts b/src/log.ts index 2b775da7e18e70212b8e4c48abbda2576c4d98f4..410fb5bf98413020b4587b2def61e4044416e2a6 100644 --- a/src/log.ts +++ b/src/log.ts @@ -59,7 +59,7 @@ export class Log { private static setupFileTransport(config: LoggingFile): transports.FileTransportInstance { config = Object.assign(new LoggingFile(), config); - const filterOutMods = format((info, opts) => { + const filterOutMods = format((info, _) => { if (config.disabled.includes(info.module) && config.enabled.length > 0 && !config.enabled.includes(info.module) @@ -70,15 +70,15 @@ export class Log { }); const opts = { - filename: config.file, - maxFiles: config.maxFiles, - maxSize: config.maxSize, datePattern: config.datePattern, - level: config.level, + filename: config.file, format: format.combine( filterOutMods(), FORMAT_FUNC, ), + level: config.level, + maxFiles: config.maxFiles, + maxSize: config.maxSize, }; return new (transports as any).DailyRotateFile(opts); diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts index bfce1154dce66d63ac1861f10c7e293ae053ac96..399a098bb36f16a24fc5e9d5df1ff80c85b7cf3a 100644 --- a/src/matrixeventprocessor.ts +++ b/src/matrixeventprocessor.ts @@ -1,12 +1,12 @@ import * as Discord from "discord.js"; -import {MessageProcessorOpts, MessageProcessor} from "./messageprocessor"; -import {DiscordBot} from "./bot"; -import {DiscordBridgeConfig} from "./config"; +import { MessageProcessorOpts, MessageProcessor } from "./messageprocessor"; +import { DiscordBot } from "./bot"; +import { DiscordBridgeConfig } from "./config"; import * as escapeStringRegexp from "escape-string-regexp"; -import {Util} from "./util"; +import { Util } from "./util"; import * as path from "path"; import * as mime from "mime"; -import {MatrixUser} from "matrix-appservice-bridge"; +import { MatrixUser } from "matrix-appservice-bridge"; import { Log } from "./log"; const log = new Log("MatrixEventProcessor"); @@ -201,8 +201,8 @@ export class MatrixEventProcessor { size = attachment.byteLength; if (size < MaxFileSize) { return { - name, attachment, + name, }; } } diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts index bb63c0956b85814028dd1e91da4956fb9bec794a..ae8c52a36debd0e7b80aad9ac9c601b524da4781 100644 --- a/src/matrixroomhandler.ts +++ b/src/matrixroomhandler.ts @@ -1,12 +1,12 @@ import { DiscordBot } from "./bot"; import { - Bridge, - RemoteRoom, - thirdPartyLookup, - thirdPartyProtocolResult, - thirdPartyUserResult, - thirdPartyLocationResult, - } from "matrix-appservice-bridge"; + Bridge, + RemoteRoom, + thirdPartyLookup, + thirdPartyProtocolResult, + thirdPartyUserResult, + thirdPartyLocationResult, +} from "matrix-appservice-bridge"; import { DiscordBridgeConfig } from "./config"; import * as Discord from "discord.js"; @@ -37,519 +37,521 @@ const JOIN_ROOM_SCHEDULE = [ export class MatrixRoomHandler { - private config: DiscordBridgeConfig; - private bridge: Bridge; - private discord: DiscordBot; - private botUserId: string; - constructor(discord: DiscordBot, config: DiscordBridgeConfig, botUserId: string, private provisioner: Provisioner) { - this.discord = discord; - this.config = config; - this.botUserId = botUserId; - } - - public get ThirdPartyLookup(): thirdPartyLookup { - return { - protocols: ["discord"], - getProtocol: this.tpGetProtocol.bind(this), - getLocation: this.tpGetLocation.bind(this), - parseLocation: this.tpParseLocation.bind(this), - getUser: this.tpGetUser.bind(this), - parseUser: this.tpParseUser.bind(this), - }; - } - - public setBridge(bridge: Bridge) { - this.bridge = bridge; - } - - public async OnAliasQueried(alias: string, roomId: string) { - log.verbose("OnAliasQueried", `Got OnAliasQueried for ${alias} ${roomId}`); - const channel = await this.discord.GetChannelFromRoomId(roomId) as Discord.GuildChannel; - - // Fire and forget RoomDirectory mapping - this.bridge.getIntent().getClient().setRoomDirectoryVisibilityAppService( - channel.guild.id, - roomId, - "public", - ); - await this.discord.ChannelSyncroniser.OnUpdate(channel); - let promiseChain: Bluebird<any> = Bluebird.resolve(); - /* We delay the joins to give some implementations a chance to breathe */ - // Join a whole bunch of users. - /* We delay the joins to give some implementations a chance to breathe */ - let delay = this.config.limits.roomGhostJoinDelay; - for (const member of (channel as Discord.TextChannel).members.array()) { - if (member.id === this.discord.GetBotId()) { - continue; + private config: DiscordBridgeConfig; + private bridge: Bridge; + private discord: DiscordBot; + private botUserId: string; + constructor(discord: DiscordBot, config: DiscordBridgeConfig, botUserId: string, private provisioner: Provisioner) { + this.discord = discord; + this.config = config; + this.botUserId = botUserId; + } + + public get ThirdPartyLookup(): thirdPartyLookup { + return { + getLocation: this.tpGetLocation.bind(this), + getProtocol: this.tpGetProtocol.bind(this), + getUser: this.tpGetUser.bind(this), + parseLocation: this.tpParseLocation.bind(this), + parseUser: this.tpParseUser.bind(this), + protocols: ["discord"], + }; + } + + public setBridge(bridge: Bridge) { + this.bridge = bridge; + } + + public async OnAliasQueried(alias: string, roomId: string) { + log.verbose("OnAliasQueried", `Got OnAliasQueried for ${alias} ${roomId}`); + const channel = await this.discord.GetChannelFromRoomId(roomId) as Discord.GuildChannel; + + // Fire and forget RoomDirectory mapping + this.bridge.getIntent().getClient().setRoomDirectoryVisibilityAppService( + channel.guild.id, + roomId, + "public", + ); + await this.discord.ChannelSyncroniser.OnUpdate(channel); + let promiseChain: Bluebird<any> = Bluebird.resolve(); + /* We delay the joins to give some implementations a chance to breathe */ + // Join a whole bunch of users. + /* We delay the joins to give some implementations a chance to breathe */ + let delay = this.config.limits.roomGhostJoinDelay; + for (const member of (channel as Discord.TextChannel).members.array()) { + if (member.id === this.discord.GetBotId()) { + continue; + } + promiseChain = promiseChain.return(Bluebird.delay(delay).then(() => { + log.info("OnAliasQueried", `UserSyncing ${member.id}`); + // Ensure the profile is up to date. + return this.discord.UserSyncroniser.OnUpdateUser(member.user); + }).then(() => { + log.info("OnAliasQueried", `Joining ${member.id} to ${roomId}`); + return this.joinRoom(this.discord.GetIntentFromDiscordMember(member), roomId) + .then(() => { + // set the correct discord guild name + this.discord.UserSyncroniser.EnsureJoin(member, roomId); + }); + })); + delay += this.config.limits.roomGhostJoinDelay; + } + // tslint:disable-next-line:await-promise + await promiseChain; + } + + public OnEvent(request, context): Promise<any> { + const event = request.getData(); + if (event.unsigned.age > AGE_LIMIT) { + log.warn(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`); + return Promise.reject("Event too old"); } - promiseChain = promiseChain.return(Bluebird.delay(delay).then(() => { - log.info("OnAliasQueried", `UserSyncing ${member.id}`); - // Ensure the profile is up to date. - return this.discord.UserSyncroniser.OnUpdateUser(member.user); - }).then(() => { - log.info("OnAliasQueried", `Joining ${member.id} to ${roomId}`); - return this.joinRoom(this.discord.GetIntentFromDiscordMember(member), roomId) - .then(() => { - // set the correct discord guild name - this.discord.UserSyncroniser.EnsureJoin(member, roomId); + if (event.type === "m.room.member" && event.content.membership === "invite") { + return this.HandleInvite(event); + } else if (event.type === "m.room.member" && event.content.membership === "join") { + if (this.bridge.getBot().isRemoteUser(event.state_key)) { + return this.discord.UserSyncroniser.OnMemberState(event, USERSYNC_STATE_DELAY_MS); + } else { + return this.discord.ProcessMatrixStateEvent(event); + } + } else if (["m.room.member", "m.room.name", "m.room.topic"].includes(event.type)) { + return this.discord.ProcessMatrixStateEvent(event); + } else if (event.type === "m.room.redaction" && context.rooms.remote) { + return this.discord.ProcessMatrixRedact(event); + } else if (event.type === "m.room.message" || event.type === "m.sticker") { + log.verbose(`Got ${event.type} event`); + const isBotCommand = event.type === "m.room.message" && + event.content.body && + event.content.body.startsWith("!discord"); + if (isBotCommand) { + return this.ProcessCommand(event, context); + } else if (context.rooms.remote) { + const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS); + return this.discord.ProcessMatrixMsgEvent(event, srvChanPair[0], srvChanPair[1]).catch((err) => { + log.warn("There was an error sending a matrix event", err); }); - })); - delay += this.config.limits.roomGhostJoinDelay; + } + } else if (event.type === "m.room.encryption" && context.rooms.remote) { + return this.HandleEncryptionWarning(event.room_id).catch((err) => { + return Promise.reject(`Failed to handle encrypted room, ${err}`); + }); + } else { + log.verbose("Got non m.room.message event"); + } + return Promise.reject("Event not processed by bridge"); } - // tslint:disable-next-line:await-promise - await promiseChain; - } - - public OnEvent(request, context): Promise<any> { - const event = request.getData(); - if (event.unsigned.age > AGE_LIMIT) { - log.warn(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`); - return Promise.reject("Event too old"); + + public async HandleEncryptionWarning(roomId: string): Promise<void> { + const intent = this.bridge.getIntent(); + log.info(`User has turned on encryption in ${roomId}, so leaving.`); + /* N.B 'status' is not specced but https://github.com/matrix-org/matrix-doc/pull/828 + has been open for over a year with no resolution. */ + const sendPromise = intent.sendMessage(roomId, { + body: "You have turned on encryption in this room, so the service will not bridge any new messages.", + msgtype: "m.notice", + status: "critical", + }); + const channel = await this.discord.GetChannelFromRoomId(roomId); + await (channel as Discord.TextChannel).send( + "Someone on Matrix has turned on encryption in this room, so the service will not bridge any new messages", + ); + await sendPromise; + await intent.leave(roomId); + await this.bridge.getRoomStore().removeEntriesByMatrixRoomId(roomId); } - if (event.type === "m.room.member" && event.content.membership === "invite") { - return this.HandleInvite(event); - } else if (event.type === "m.room.member" && event.content.membership === "join") { - if (this.bridge.getBot().isRemoteUser(event.state_key)) { - return this.discord.UserSyncroniser.OnMemberState(event, USERSYNC_STATE_DELAY_MS); + + public HandleInvite(event: any) { + log.info("Received invite for " + event.state_key + " in room " + event.room_id); + if (event.state_key === this.botUserId) { + log.info("Accepting invite for bridge bot"); + return this.joinRoom(this.bridge.getIntent(), event.room_id); } else { - return this.discord.ProcessMatrixStateEvent(event); + return this.discord.ProcessMatrixStateEvent(event); + } + } + + public async ProcessCommand(event: any, context: any) { + const intent = this.bridge.getIntent(); + // Due to #257 we need to check if we are joined. + try { + await intent.getClient().sendReadReceipt(event.event_id); + } catch (ex) { + log.warn("Couldn't send a read reciept into the room:", ex, ". Ignoring command."); + return; } - } else if (["m.room.member", "m.room.name", "m.room.topic"].includes(event.type)) { - return this.discord.ProcessMatrixStateEvent(event); - } else if (event.type === "m.room.redaction" && context.rooms.remote) { - return this.discord.ProcessMatrixRedact(event); - } else if (event.type === "m.room.message" || event.type === "m.sticker") { - log.verbose(`Got ${event.type} event`); - const isBotCommand = event.type === "m.room.message" && - event.content.body && - event.content.body.startsWith("!discord"); - if (isBotCommand) { - return this.ProcessCommand(event, context); - } else if (context.rooms.remote) { - const srvChanPair = context.rooms.remote.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS); - return this.discord.ProcessMatrixMsgEvent(event, srvChanPair[0], srvChanPair[1]).catch((err) => { - log.warn("There was an error sending a matrix event", err); + + if (!this.config.bridge.enableSelfServiceBridging) { + // We can do this here because the only commands we support are self-service bridging + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "The owner of this bridge does not permit self-service bridging.", + msgtype: "m.notice", }); } - } else if (event.type === "m.room.encryption" && context.rooms.remote) { - return this.HandleEncryptionWarning(event.room_id).catch((err) => { - return Promise.reject(`Failed to handle encrypted room, ${err}`); + + // Check to make sure the user has permission to do anything in the room. We can do this here + // because the only commands we support are self-service commands (which therefore require some + // level of permissions) + const plEvent = await this.bridge.getIntent().getClient() + .getStateEvent(event.room_id, "m.room.power_levels", ""); + let userLevel = PROVISIONING_DEFAULT_USER_POWER_LEVEL; + let requiredLevel = PROVISIONING_DEFAULT_POWER_LEVEL; + if (plEvent && plEvent.state_default) { + requiredLevel = plEvent.state_default; + } + if (plEvent && plEvent.users_default) { + userLevel = plEvent.users_default; + } + if (plEvent && plEvent.users && plEvent.users[event.sender]) { + userLevel = plEvent.users[event.sender]; + } + + if (userLevel < requiredLevel) { + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "You do not have the required power level in this room to create a bridge to a Discord channel.", + msgtype: "m.notice", + }); + } + + const {command, args} = Util.MsgToArgs(event.content.body, "!discord"); + + if (command === "help" && args[0] === "bridge") { + const link = Util.GetBotLink(this.config); + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "How to bridge a Discord guild:\n" + + "1. Invite the bot to your Discord guild using this link: " + link + "\n" + + "2. Invite me to the matrix room you'd like to bridge\n" + + "3. Open the Discord channel you'd like to bridge in a web browser\n" + + "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " + + "(without the backticks)\n" + + " Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" + + " The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" + + "5. Enjoy your new bridge!", + msgtype: "m.notice", + }); + } else if (command === "bridge") { + if (context.rooms.remote) { + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "This room is already bridged to a Discord guild.", + msgtype: "m.notice", + }); + } + + const minArgs = 2; + if (args.length < minArgs) { + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "Invalid syntax. For more information try !discord help bridge", + msgtype: "m.notice", + }); + } + + const guildId = args[0]; + const channelId = args[1]; + try { + const discordResult = await this.discord.LookupRoom(guildId, channelId); + const channel = discordResult.channel as Discord.TextChannel; + + log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`); + this.bridge.getIntent().sendMessage(event.room_id, { + body: "I'm asking permission from the guild administrators to make this bridge.", + msgtype: "m.notice", + }); + + await this.provisioner.AskBridgePermission(channel, event.sender); + this.provisioner.BridgeMatrixRoom(channel, event.room_id); + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "I have bridged this room to your channel", + msgtype: "m.notice", + }); + } catch (err) { + if (err.message === "Timed out waiting for a response from the Discord owners" + || err.message === "The bridge has been declined by the Discord guild") { + return this.bridge.getIntent().sendMessage(event.room_id, { + body: err.message, + msgtype: "m.notice", + }); + } + + log.error(`Error bridging ${event.room_id} to ${guildId}/${channelId}`); + log.error(err); + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "There was a problem bridging that channel - has the guild owner approved the bridge?", + msgtype: "m.notice", + }); + } + } else if (command === "unbridge") { + const remoteRoom = context.rooms.remote; + + if (!remoteRoom) { + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "This room is not bridged.", + msgtype: "m.notice", + }); + } + + if (!remoteRoom.data.plumbed) { + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "This room cannot be unbridged.", + msgtype: "m.notice", + }); + } + + try { + await this.provisioner.UnbridgeRoom(remoteRoom); + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "This room has been unbridged", + msgtype: "m.notice", + }); + } catch (err) { + log.error("Error while unbridging room " + event.room_id); + log.error(err); + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "There was an error unbridging this room. " + + "Please try again later or contact the bridge operator.", + msgtype: "m.notice", + }); + } + } else if (command === "help") { + // Unknown command or no command given to get help on, so we'll just give them the help + return this.bridge.getIntent().sendMessage(event.room_id, { + body: "Available commands:\n" + + "!discord bridge <guild id> <channel id> - Bridges this room to a Discord channel\n" + + "!discord unbridge - Unbridges a Discord channel from this room\n" + + "!discord help <command> - Help menu for another command. Eg: !discord help bridge\n", + msgtype: "m.notice", + }); + } + } + + public OnAliasQuery(alias: string, aliasLocalpart: string): Promise<any> { + log.info("Got request for #", aliasLocalpart); + const srvChanPair = aliasLocalpart.substr("_discord_".length).split("_", ROOM_NAME_PARTS); + if (srvChanPair.length < ROOM_NAME_PARTS || srvChanPair[0] === "" || srvChanPair[1] === "") { + log.warn(`Alias '${aliasLocalpart}' was missing a server and/or a channel`); + return; + } + return this.discord.LookupRoom(srvChanPair[0], srvChanPair[1]).then((result) => { + log.info("Creating #", aliasLocalpart); + return this.createMatrixRoom(result.channel, aliasLocalpart); + }).catch((err) => { + log.error(`Couldn't find discord room '${aliasLocalpart}'.`, err); }); - } else { - log.verbose("Got non m.room.message event"); } - return Promise.reject("Event not processed by bridge"); - } - - public async HandleEncryptionWarning(roomId: string): Promise<void> { - const intent = this.bridge.getIntent(); - log.info(`User has turned on encryption in ${roomId}, so leaving.`); - /* N.B 'status' is not specced but https://github.com/matrix-org/matrix-doc/pull/828 - has been open for over a year with no resolution. */ - const sendPromise = intent.sendMessage(roomId, { - msgtype: "m.notice", - status: "critical", - body: "You have turned on encryption in this room, so the service will not bridge any new messages.", - }); - const channel = await this.discord.GetChannelFromRoomId(roomId); - await (channel as Discord.TextChannel).send( - "Someone on Matrix has turned on encryption in this room, so the service will not bridge any new messages", - ); - await sendPromise; - await intent.leave(roomId); - await this.bridge.getRoomStore().removeEntriesByMatrixRoomId(roomId); - } - - public HandleInvite(event: any) { - log.info("Received invite for " + event.state_key + " in room " + event.room_id); - if (event.state_key === this.botUserId) { - log.info("Accepting invite for bridge bot"); - return this.joinRoom(this.bridge.getIntent(), event.room_id); - } else { - return this.discord.ProcessMatrixStateEvent(event); + + public tpGetProtocol(protocol: string): Promise<thirdPartyProtocolResult> { + return Promise.resolve({ + field_types: { + // guild_name: { + // regexp: "\S.{0,98}\S", + // placeholder: "Guild", + // }, + channel_id: { + placeholder: "", + regexp: "[0-9]*", + }, + channel_name: { + placeholder: "#Channel", + regexp: "[A-Za-z0-9_\-]{2,100}", + }, + discriminator: { + placeholder: "1234", + regexp: "[0-9]{4}", + }, + guild_id: { + placeholder: "", + regexp: "[0-9]*", + }, + username: { + placeholder: "Username", + regexp: "[A-Za-z0-9_\-]{2,100}", + }, + }, + instances: this.discord.GetGuilds().map((guild) => { + return { + bot_user_id: this.botUserId, + desc: guild.name, + fields: { + guild_id: guild.id, + }, + icon: guild.iconURL || ICON_URL, // TODO: Use icons from our content repo. Potential security risk. + network_id: guild.id, + }; + }), + location_fields: ["guild_id", "channel_name"], + user_fields: ["username", "discriminator"], + }); } - } - - public async ProcessCommand(event: any, context: any) { - const intent = this.bridge.getIntent(); - // Due to #257 we need to check if we are joined. - try { - await intent.getClient().sendReadReceipt(event.event_id); - } catch (ex) { - log.warn("Couldn't send a read reciept into the room:", ex, ". Ignoring command."); - return; - } - - if (!this.config.bridge.enableSelfServiceBridging) { - // We can do this here because the only commands we support are self-service bridging - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "The owner of this bridge does not permit self-service bridging.", - }); - } - - // Check to make sure the user has permission to do anything in the room. We can do this here - // because the only commands we support are self-service commands (which therefore require some - // level of permissions) - const plEvent = await this.bridge.getIntent().getClient().getStateEvent(event.room_id, "m.room.power_levels", ""); - let userLevel = PROVISIONING_DEFAULT_USER_POWER_LEVEL; - let requiredLevel = PROVISIONING_DEFAULT_POWER_LEVEL; - if (plEvent && plEvent.state_default) { - requiredLevel = plEvent.state_default; - } - if (plEvent && plEvent.users_default) { - userLevel = plEvent.users_default; - } - if (plEvent && plEvent.users && plEvent.users[event.sender]) { - userLevel = plEvent.users[event.sender]; - } - - if (userLevel < requiredLevel) { - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "You do not have the required power level in this room to create a bridge to a Discord channel.", - }); - } - - const {command, args} = Util.MsgToArgs(event.content.body, "!discord"); - - if (command === "help" && args[0] === "bridge") { - const link = Util.GetBotLink(this.config); - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "How to bridge a Discord guild:\n" + - "1. Invite the bot to your Discord guild using this link: " + link + "\n" + - "2. Invite me to the matrix room you'd like to bridge\n" + - "3. Open the Discord channel you'd like to bridge in a web browser\n" + - "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " + - "(without the backticks)\n" + - " Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" + - " The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" + - "5. Enjoy your new bridge!", - }); - } else if (command === "bridge") { - if (context.rooms.remote) { - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "This room is already bridged to a Discord guild.", - }); - } - - const minArgs = 2; - if (args.length < minArgs) { - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "Invalid syntax. For more information try !discord help bridge", - }); - } - - const guildId = args[0]; - const channelId = args[1]; - try { - const discordResult = await this.discord.LookupRoom(guildId, channelId); - const channel = discordResult.channel as Discord.TextChannel; - - log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`); - this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "I'm asking permission from the guild administrators to make this bridge.", - }); - - await this.provisioner.AskBridgePermission(channel, event.sender); - this.provisioner.BridgeMatrixRoom(channel, event.room_id); - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "I have bridged this room to your channel", - }); - } catch (err) { - if (err.message === "Timed out waiting for a response from the Discord owners" - || err.message === "The bridge has been declined by the Discord guild") { - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: err.message, - }); - } - - log.error(`Error bridging ${event.room_id} to ${guildId}/${channelId}`); - log.error(err); - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "There was a problem bridging that channel - has the guild owner approved the bridge?", - }); - } - } else if (command === "unbridge") { - const remoteRoom = context.rooms.remote; - - if (!remoteRoom) { - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "This room is not bridged.", - }); - } - - if (!remoteRoom.data.plumbed) { - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "This room cannot be unbridged.", - }); - } - - try { - await this.provisioner.UnbridgeRoom(remoteRoom); - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "This room has been unbridged", - }); - } catch (err) { - log.error("Error while unbridging room " + event.room_id); - log.error(err); - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "There was an error unbridging this room. " + - "Please try again later or contact the bridge operator.", - }); - } - } else if (command === "help") { - // Unknown command or no command given to get help on, so we'll just give them the help - return this.bridge.getIntent().sendMessage(event.room_id, { - msgtype: "m.notice", - body: "Available commands:\n" + - "!discord bridge <guild id> <channel id> - Bridges this room to a Discord channel\n" + - "!discord unbridge - Unbridges a Discord channel from this room\n" + - "!discord help <command> - Help menu for another command. Eg: !discord help bridge\n", - }); - } - } - - public OnAliasQuery(alias: string, aliasLocalpart: string): Promise<any> { - log.info("Got request for #", aliasLocalpart); - const srvChanPair = aliasLocalpart.substr("_discord_".length).split("_", ROOM_NAME_PARTS); - if (srvChanPair.length < ROOM_NAME_PARTS || srvChanPair[0] === "" || srvChanPair[1] === "") { - log.warn(`Alias '${aliasLocalpart}' was missing a server and/or a channel`); - return; + + public tpGetLocation(protocol: string, fields: any): Promise<thirdPartyLocationResult[]> { + log.info("Got location request ", protocol, fields); + const chans = this.discord.ThirdpartySearchForChannels(fields.guild_id, fields.channel_name); + return Promise.resolve(chans); } - return this.discord.LookupRoom(srvChanPair[0], srvChanPair[1]).then((result) => { - log.info("Creating #", aliasLocalpart); - return this.createMatrixRoom(result.channel, aliasLocalpart); - }).catch((err) => { - log.error(`Couldn't find discord room '${aliasLocalpart}'.`, err); - }); - } - - public tpGetProtocol(protocol: string): Promise<thirdPartyProtocolResult> { - return Promise.resolve({ - user_fields: ["username", "discriminator"], - location_fields: ["guild_id", "channel_name"], - field_types: { - // guild_name: { - // regexp: "\S.{0,98}\S", - // placeholder: "Guild", - // }, - guild_id: { - regexp: "[0-9]*", - placeholder: "", - }, - channel_id: { - regexp: "[0-9]*", - placeholder: "", - }, - channel_name: { - regexp: "[A-Za-z0-9_\-]{2,100}", - placeholder: "#Channel", - }, - username: { - regexp: "[A-Za-z0-9_\-]{2,100}", - placeholder: "Username", - }, - discriminator: { - regexp: "[0-9]{4}", - placeholder: "1234", - }, - }, - instances: this.discord.GetGuilds().map((guild) => { - return { - network_id: guild.id, - bot_user_id: this.botUserId, - desc: guild.name, - icon: guild.iconURL || ICON_URL, // TODO: Use icons from our content repo. Potential security risk. - fields: { - guild_id: guild.id, - }, - }; - }), - }); - } - - public tpGetLocation(protocol: string, fields: any): Promise<thirdPartyLocationResult[]> { - log.info("Got location request ", protocol, fields); - const chans = this.discord.ThirdpartySearchForChannels(fields.guild_id, fields.channel_name); - return Promise.resolve(chans); - } - - public tpParseLocation(alias: string): Promise<thirdPartyLocationResult[]> { - return Promise.reject({err: "Unsupported", code: HTTP_UNSUPPORTED}); - } - - public tpGetUser(protocol: string, fields: any): Promise<thirdPartyUserResult[]> { - log.info("Got user request ", protocol, fields); - return Promise.reject({err: "Unsupported", code: HTTP_UNSUPPORTED}); - } - - public tpParseUser(userid: string): Promise<thirdPartyUserResult[]> { - return Promise.reject({err: "Unsupported", code: HTTP_UNSUPPORTED}); - } - - public async HandleDiscordCommand(msg: Discord.Message) { - if (!(msg.channel as Discord.TextChannel).guild) { - msg.channel.send("**ERROR:** only available for guild channels"); + + public tpParseLocation(alias: string): Promise<thirdPartyLocationResult[]> { + return Promise.reject({err: "Unsupported", code: HTTP_UNSUPPORTED}); + } + + public tpGetUser(protocol: string, fields: any): Promise<thirdPartyUserResult[]> { + log.info("Got user request ", protocol, fields); + return Promise.reject({err: "Unsupported", code: HTTP_UNSUPPORTED}); } - const {command, args} = Util.MsgToArgs(msg.content, "!matrix"); - - const intent = this.bridge.getIntent(); - - const actions: ICommandActions = { - kick: { - params: ["name"], - description: "Kicks a user on the matrix side", - permission: "KICK_MEMBERS", - run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "kick", "Kicked"), - }, - ban: { - params: ["name"], - description: "Bans a user on the matrix side", - permission: "BAN_MEMBERS", - run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "ban", "Banned"), - }, - unban: { - params: ["name"], - description: "Unbans a user on the matrix side", - permission: "BAN_MEMBERS", - run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "unban", "Unbanned"), - }, - }; - - const parameters: ICommandParameters = { - name: { - description: "The display name or mxid of a matrix user", - get: async (name) => { - const channelMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(msg.channel); - const mxUserId = await Util.GetMxidFromName(intent, name, channelMxids); - return mxUserId; - }, - }, - }; - - if (command === "help") { - let replyMessage = "Available Commands:\n"; - for (const actionKey of Object.keys(actions)) { - const action = actions[actionKey]; - if (!msg.member.hasPermission(action.permission as any)) { - continue; + public tpParseUser(userid: string): Promise<thirdPartyUserResult[]> { + return Promise.reject({err: "Unsupported", code: HTTP_UNSUPPORTED}); + } + + public async HandleDiscordCommand(msg: Discord.Message) { + if (!(msg.channel as Discord.TextChannel).guild) { + msg.channel.send("**ERROR:** only available for guild channels"); } - replyMessage += " - `!matrix " + actionKey; - for (const param of action.params) { - replyMessage += ` <${param}>`; + + const {command, args} = Util.MsgToArgs(msg.content, "!matrix"); + + const intent = this.bridge.getIntent(); + + const actions: ICommandActions = { + ban: { + description: "Bans a user on the matrix side", + params: ["name"], + permission: "BAN_MEMBERS", + run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "ban", "Banned"), + }, + kick: { + description: "Kicks a user on the matrix side", + params: ["name"], + permission: "KICK_MEMBERS", + run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "kick", "Kicked"), + }, + unban: { + description: "Unbans a user on the matrix side", + params: ["name"], + permission: "BAN_MEMBERS", + run: this.DiscordModerationActionGenerator(msg.channel as Discord.TextChannel, "unban", "Unbanned"), + }, + }; + + const parameters: ICommandParameters = { + name: { + description: "The display name or mxid of a matrix user", + get: async (name) => { + const channelMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(msg.channel); + const mxUserId = await Util.GetMxidFromName(intent, name, channelMxids); + return mxUserId; + }, + }, + }; + + if (command === "help") { + let replyHelpMessage = "Available Commands:\n"; + for (const actionKey of Object.keys(actions)) { + const action = actions[actionKey]; + if (!msg.member.hasPermission(action.permission as any)) { + continue; + } + replyHelpMessage += " - `!matrix " + actionKey; + for (const param of action.params) { + replyHelpMessage += ` <${param}>`; + } + replyHelpMessage += "`: " + action.description + "\n"; + } + replyHelpMessage += "\nParameters:\n"; + for (const parameterKey of Object.keys(parameters)) { + const parameter = parameters[parameterKey]; + replyHelpMessage += " - `<" + parameterKey + ">`: " + parameter.description + "\n"; + } + msg.channel.send(replyHelpMessage); + return; } - replyMessage += "`: " + action.description + "\n"; - } - replyMessage += "\nParameters:\n"; - for (const parameterKey of Object.keys(parameters)) { - const parameter = parameters[parameterKey]; - replyMessage += " - `<" + parameterKey + ">`: " + parameter.description + "\n"; - } - msg.channel.send(replyMessage); - return; - } - if (!actions[command]) { - msg.channel.send("**Error:** unknown command. Try `!matrix help` to see all commands"); - return; + if (!actions[command]) { + msg.channel.send("**Error:** unknown command. Try `!matrix help` to see all commands"); + return; + } + + if (!msg.member.hasPermission(actions[command].permission as any)) { + msg.channel.send("**ERROR:** insufficiant permissions to use this matrix command"); + return; + } + + let replyMessage = ""; + try { + replyMessage = await Util.ParseCommand(actions[command], parameters, args); + } catch (e) { + replyMessage = "**ERROR:** " + e.message; + } + + msg.channel.send(replyMessage); } - if (!msg.member.hasPermission(actions[command].permission as any)) { - msg.channel.send("**ERROR:** insufficiant permissions to use this matrix command"); - return; + private DiscordModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: string, action: string) { + return async ({name}) => { + let allChannelMxids = []; + await Promise.all(discordChannel.guild.channels.map((chan) => { + return this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(chan).then((chanMxids) => { + allChannelMxids = allChannelMxids.concat(chanMxids); + }).catch((e) => { + // pass, non-text-channel + }); + })); + let errorMsg = ""; + await Promise.all(allChannelMxids.map((chanMxid) => { + const intent = this.bridge.getIntent(); + return intent[funcKey](chanMxid, name).catch((e) => { + // maybe we don't have permission to kick/ban/unban...? + errorMsg += `\nCouldn't ${funcKey} ${name} from ${chanMxid}`; + }); + })); + if (errorMsg) { + throw Error(errorMsg); + } + return `${action} ${name}`; + }; } - let replyMessage = ""; - try { - replyMessage = await Util.ParseCommand(actions[command], parameters, args); - } catch (e) { - replyMessage = "**ERROR:** " + e.message; + private joinRoom(intent: any, roomIdOrAlias: string): Promise<string> { + let currentSchedule = JOIN_ROOM_SCHEDULE[0]; + const doJoin = () => Util.DelayedPromise(currentSchedule) + .then(() => intent.getClient().joinRoom(roomIdOrAlias)); + const errorHandler = (err) => { + log.error(`Error joining room ${roomIdOrAlias} as ${intent.getClient().getUserId()}`); + log.error(err); + const idx = JOIN_ROOM_SCHEDULE.indexOf(currentSchedule); + if (idx === JOIN_ROOM_SCHEDULE.length - 1) { + log.warn(`Cannot join ${roomIdOrAlias} as ${intent.getClient().getUserId()}`); + return Promise.reject(err); + } else { + currentSchedule = JOIN_ROOM_SCHEDULE[idx + 1]; + return doJoin().catch(errorHandler); + } + }; + + return doJoin().catch(errorHandler); } - msg.channel.send(replyMessage); - } - - private DiscordModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: string, action: string) { - return async ({name}) => { - let allChannelMxids = []; - await Promise.all(discordChannel.guild.channels.map((chan) => { - return this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(chan).then((chanMxids) => { - allChannelMxids = allChannelMxids.concat(chanMxids); - }).catch((e) => { - // pass, non-text-channel - }); - })); - let errorMsg = ""; - await Promise.all(allChannelMxids.map((chanMxid) => { - const intent = this.bridge.getIntent(); - return intent[funcKey](chanMxid, name).catch((e) => { - // maybe we don't have permission to kick/ban/unban...? - errorMsg += `\nCouldn't ${funcKey} ${name} from ${chanMxid}`; - }); - })); - if (errorMsg) { - throw Error(errorMsg); - } - return `${action} ${name}`; - }; - } - - private joinRoom(intent: any, roomIdOrAlias: string): Promise<string> { - let currentSchedule = JOIN_ROOM_SCHEDULE[0]; - const doJoin = () => Util.DelayedPromise(currentSchedule).then(() => intent.getClient().joinRoom(roomIdOrAlias)); - const errorHandler = (err) => { - log.error(`Error joining room ${roomIdOrAlias} as ${intent.getClient().getUserId()}`); - log.error(err); - const idx = JOIN_ROOM_SCHEDULE.indexOf(currentSchedule); - if (idx === JOIN_ROOM_SCHEDULE.length - 1) { - log.warn(`Cannot join ${roomIdOrAlias} as ${intent.getClient().getUserId()}`); - return Promise.reject(err); - } else { - currentSchedule = JOIN_ROOM_SCHEDULE[idx + 1]; - return doJoin().catch(errorHandler); - } - }; - - return doJoin().catch(errorHandler); - } - - private createMatrixRoom(channel: Discord.TextChannel, alias: string) { - const remote = new RemoteRoom(`discord_${channel.guild.id}_${channel.id}`); - remote.set("discord_type", "text"); - remote.set("discord_guild", channel.guild.id); - remote.set("discord_channel", channel.id); - remote.set("update_name", true); - remote.set("update_topic", true); - remote.set("update_icon", true); - const creationOpts = { - visibility: this.config.room.defaultVisibility, - room_alias_name: alias, - initial_state: [ - { - type: "m.room.join_rules", - content: { - join_rule: "public", - }, - state_key: "", - }, - ], - }; - return { - creationOpts, - remote, - }; - } + private createMatrixRoom(channel: Discord.TextChannel, alias: string) { + const remote = new RemoteRoom(`discord_${channel.guild.id}_${channel.id}`); + remote.set("discord_type", "text"); + remote.set("discord_guild", channel.guild.id); + remote.set("discord_channel", channel.id); + remote.set("update_name", true); + remote.set("update_topic", true); + remote.set("update_icon", true); + const creationOpts = { + initial_state: [ + { + content: { + join_rule: "public", + }, + state_key: "", + type: "m.room.join_rules", + }, + ], + room_alias_name: alias, + visibility: this.config.room.defaultVisibility, + }; + return { + creationOpts, + remote, + }; + } } diff --git a/src/presencehandler.ts b/src/presencehandler.ts index 88a6cc8ac74f206e263b327a43a75f8428fa551e..8e2c9964551bcd3c33073849361e66f7e40dbf37 100644 --- a/src/presencehandler.ts +++ b/src/presencehandler.ts @@ -1,4 +1,4 @@ -import {User, Presence} from "discord.js"; +import { User, Presence } from "discord.js"; import { DiscordBot } from "./bot"; import { Log } from "./log"; const log = new Log("PresenceHandler"); diff --git a/src/provisioner.ts b/src/provisioner.ts index c8a2e0d5cb559b0f79173f493058ca3979a381af..7bbb286f3c4dcf35b84259f0c1cae6d7079365ae 100644 --- a/src/provisioner.ts +++ b/src/provisioner.ts @@ -4,7 +4,6 @@ import { MatrixRoom, } from "matrix-appservice-bridge"; import * as Discord from "discord.js"; -import { Permissions } from "discord.js"; const PERMISSION_REQUEST_TIMEOUT = 300000; // 5 minutes @@ -76,7 +75,7 @@ export class Provisioner { } const perms = channel.permissionsFor(member); - if (!perms.hasPermission(Permissions.FLAGS.MANAGE_WEBHOOKS)) { + if (!perms.hasPermission(Discord.Permissions.FLAGS.MANAGE_WEBHOOKS)) { // Missing permissions, so just reject it return Promise.reject(new Error("You do not have permission to manage webhooks in this channel")); } diff --git a/src/store.ts b/src/store.ts index 56769f9b5455b2370cd210ae25a70c7012423b94..5dc491c2efe674d283ec9042119fbb344db06f52 100644 --- a/src/store.ts +++ b/src/store.ts @@ -13,293 +13,293 @@ const log = new Log("DiscordStore"); * Stores data for specific users and data not specific to rooms. */ export class DiscordStore { - /** - * @param {string} filepath Location of the SQLite database file. - */ - public db: IDatabaseConnector; - private version: number; - private config: DiscordBridgeConfigDatabase; - constructor(private configOrFile: DiscordBridgeConfigDatabase|string) { - if (typeof(configOrFile) === "string") { - this.config = new DiscordBridgeConfigDatabase(); - this.config.filename = configOrFile; - } else { - this.config = configOrFile; + /** + * @param {string} filepath Location of the SQLite database file. + */ + public db: IDatabaseConnector; + private version: number; + private config: DiscordBridgeConfigDatabase; + constructor(private configOrFile: DiscordBridgeConfigDatabase|string) { + if (typeof(configOrFile) === "string") { + this.config = new DiscordBridgeConfigDatabase(); + this.config.filename = configOrFile; + } else { + this.config = configOrFile; + } + this.version = null; } - this.version = null; - } - public backup_database(): Promise<void|{}> { - if (this.config.filename == null) { - log.warn("Backups not supported on non-sqlite connector"); - return; - } - if (this.config.filename === ":memory:") { - log.info("Can't backup a :memory: database."); - return Promise.resolve(); + public backup_database(): Promise<void|{}> { + if (this.config.filename == null) { + log.warn("Backups not supported on non-sqlite connector"); + return; + } + if (this.config.filename === ":memory:") { + log.info("Can't backup a :memory: database."); + return Promise.resolve(); + } + const BACKUP_NAME = this.config.filename + ".backup"; + + return new Promise((resolve, reject) => { + // Check to see if a backup file already exists. + fs.access(BACKUP_NAME, (err) => { + return resolve(err === null); + }); + }).then((result) => { + return new Promise((resolve, reject) => { + if (!result) { + log.warn("NOT backing up database while a file already exists"); + resolve(true); + } + const rd = fs.createReadStream(this.config.filename); + rd.on("error", reject); + const wr = fs.createWriteStream(BACKUP_NAME); + wr.on("error", reject); + wr.on("close", resolve); + rd.pipe(wr); + }); + }); } - const BACKUP_NAME = this.config.filename + ".backup"; - return new Promise((resolve, reject) => { - // Check to see if a backup file already exists. - fs.access(BACKUP_NAME, (err) => { - return resolve(err === null); - }); - }).then((result) => { - return new Promise((resolve, reject) => { - if (!result) { - log.warn("NOT backing up database while a file already exists"); - resolve(true); + /** + * Checks the database has all the tables needed. + */ + public async init(overrideSchema: number = 0): Promise<void> { + log.info("Starting DB Init"); + await this.open_database(); + let version = await this.getSchemaVersion(); + const targetSchema = overrideSchema || CURRENT_SCHEMA; + while (version < targetSchema) { + version++; + const schemaClass = require(`./db/schema/v${version}.js`).Schema; + const schema = (new schemaClass() as IDbSchema); + log.info(`Updating database to v${version}, "${schema.description}"`); + try { + await schema.run(this); + log.info("Updated database to version ", version); + } catch (ex) { + log.error("Couldn't update database to schema ", version); + log.error(ex); + log.info("Rolling back to version ", version - 1); + try { + await schema.rollBack(this); + } catch (ex) { + log.error(ex); + throw Error("Failure to update to latest schema. And failed to rollback."); + } + throw Error("Failure to update to latest schema."); + } + this.version = version; + await this.setSchemaVersion(version); } - const rd = fs.createReadStream(this.config.filename); - rd.on("error", reject); - const wr = fs.createWriteStream(BACKUP_NAME); - wr.on("error", reject); - wr.on("close", resolve); - rd.pipe(wr); - }); - }); - } + log.info("Updated database to the latest schema"); + } - /** - * Checks the database has all the tables needed. - */ - public async init(overrideSchema: number = 0): Promise<void> { - log.info("Starting DB Init"); - await this.open_database(); - let version = await this.getSchemaVersion(); - const targetSchema = overrideSchema || CURRENT_SCHEMA; - while (version < targetSchema) { - version++; - const schemaClass = require(`./db/schema/v${version}.js`).Schema; - const schema = (new schemaClass() as IDbSchema); - log.info(`Updating database to v${version}, "${schema.description}"`); - try { - await schema.run(this); - log.info("Updated database to version ", version); - } catch (ex) { - log.error("Couldn't update database to schema ", version); - log.error(ex); - log.info("Rolling back to version ", version - 1); - try { - await schema.rollBack(this); - } catch (ex) { - log.error(ex); - throw Error("Failure to update to latest schema. And failed to rollback."); - } - throw Error("Failure to update to latest schema."); - } - this.version = version; - await this.setSchemaVersion(version); + public close() { + this.db.Close(); + } + + public create_table(statement: string, tablename: string): Promise<void|Error> { + return this.db.Exec(statement).then(() => { + log.info("Created table", tablename); + }).catch((err) => { + throw new Error(`Error creating '${tablename}': ${err}`); + }); } - log.info("Updated database to the latest schema"); - } - public close() { - this.db.Close(); - } + public add_user_token(userId: string, discordId: string, token: string): Promise<any> { + log.silly("SQL", "add_user_token => ", userId); + return Promise.all([ + this.db.Run( + ` + INSERT INTO user_id_discord_id (discord_id,user_id) VALUES ($discordId,$userId); + ` + , { + discordId, + userId, + }), + this.db.Run( + ` + INSERT INTO discord_id_token (discord_id,token) VALUES ($discordId,$token); + ` + , { + discordId, + token, + }), + ]).catch( (err) => { + log.error("Error storing user token ", err); + throw err; + }); + } - public create_table(statement: string, tablename: string): Promise<void|Error> { - return this.db.Exec(statement).then(() => { - log.info("Created table", tablename); - }).catch((err) => { - throw new Error(`Error creating '${tablename}': ${err}`); - }); - } + public delete_user_token(discordId: string): Promise<null> { + log.silly("SQL", "delete_user_token => ", discordId); + return this.db.Run( + ` + DELETE FROM user_id_discord_id WHERE discord_id = $id; + DELETE FROM discord_id_token WHERE discord_id = $id; + ` + , { + $id: discordId, + }).catch( (err) => { + log.error("Error deleting user token ", err); + throw err; + }); + } - public add_user_token(userId: string, discordId: string, token: string): Promise<any> { - log.silly("SQL", "add_user_token => ", userId); - return Promise.all([ - this.db.Run( - ` - INSERT INTO user_id_discord_id (discord_id,user_id) VALUES ($discordId,$userId); - ` + public get_user_discord_ids(userId: string): Promise<string[]> { + log.silly("SQL", "get_user_discord_ids => ", userId); + return this.db.All( + ` + SELECT discord_id + FROM user_id_discord_id + WHERE user_id = $userId; + ` , { userId, - discordId, - }), - this.db.Run( - ` - INSERT INTO discord_id_token (discord_id,token) VALUES ($discordId,$token); - ` + }).then( (rows) => { + if (rows != null) { + return rows.map((row) => row.discord_id); + } else { + return []; + } + }).catch( (err) => { + log.error("Error getting discord ids: ", err.Error); + throw err; + }); + } + + public get_token(discordId: string): Promise<string> { + log.silly("SQL", "discord_id_token => ", discordId); + return this.db.Get( + ` + SELECT token + FROM discord_id_token + WHERE discord_id = $discordId + ` , { discordId, - token, - }), - ]).catch( (err) => { - log.error("Error storing user token ", err); - throw err; - }); - } - - public delete_user_token(discordId: string): Promise<null> { - log.silly("SQL", "delete_user_token => ", discordId); - return this.db.Run( - ` - DELETE FROM user_id_discord_id WHERE discord_id = $id; - DELETE FROM discord_id_token WHERE discord_id = $id; - ` - , { - $id: discordId, - }).catch( (err) => { - log.error("Error deleting user token ", err); - throw err; - }); - } - - public get_user_discord_ids(userId: string): Promise<string[]> { - log.silly("SQL", "get_user_discord_ids => ", userId); - return this.db.All( - ` - SELECT discord_id - FROM user_id_discord_id - WHERE user_id = $userId; - `, { - userId, - }, - ).then( (rows) => { - if (rows != null) { - return rows.map((row) => row.discord_id); - } else { - return []; - } - }).catch( (err) => { - log.error("Error getting discord ids: ", err.Error); - throw err; - }); - } - - public get_token(discordId: string): Promise<string> { - log.silly("SQL", "discord_id_token => ", discordId); - return this.db.Get( - ` - SELECT token - FROM discord_id_token - WHERE discord_id = $discordId - `, { - discordId, - }, - ).then( (row) => { - return row != null ? row.token : null; - }).catch( (err) => { - log.error("Error getting discord ids ", err.Error); - throw err; - }); - } - - public get_dm_room(discordId, discordChannel): Promise<string> { - log.silly("SQL", "get_dm_room => ", discordChannel); // Don't show discordId for privacy reasons - return this.db.Get( - ` - SELECT room_id - FROM dm_rooms - WHERE dm_rooms.discord_id = $discordId - AND dm_rooms.discord_channel = $discordChannel; - ` - , { - discordId, - discordChannel, - }).then( (row) => { - return row != null ? row.room_id : null; - }).catch( (err) => { - log.error("Error getting room_id ", err.Error); - throw err; - }); - } + }).then( (row) => { + return row != null ? row.token : null; + }).catch( (err) => { + log.error("Error getting discord ids ", err.Error); + throw err; + }); + } - public set_dm_room(discordId, discordChannel, roomId): Promise<null> { - log.silly("SQL", "set_dm_room => ", discordChannel); // Don't show discordId for privacy reasons - return this.db.Run( - ` - REPLACE INTO dm_rooms (discord_id,discord_channel,room_id) - VALUES ($discordId,$discordChannel,$roomId); - ` - , { - discordId, - discordChannel, - roomId, - }).catch( (err) => { - log.error("Error executing set_dm_room query ", err.Error); - throw err; - }); - } + public get_dm_room(discordId, discordChannel): Promise<string> { + log.silly("SQL", "get_dm_room => ", discordChannel); // Don't show discordId for privacy reasons + return this.db.Get( + ` + SELECT room_id + FROM dm_rooms + WHERE dm_rooms.discord_id = $discordId + AND dm_rooms.discord_channel = $discordChannel; + ` + , { + discordChannel, + discordId, + }).then( (row) => { + return row != null ? row.room_id : null; + }).catch( (err) => { + log.error("Error getting room_id ", err.Error); + throw err; + }); + } - public get_all_user_discord_ids(): Promise<any> { - log.silly("SQL", "get_users_tokens"); - return this.db.All( - ` - SELECT * - FROM get_user_discord_ids - `, - ).then( (rows) => { - return rows; - }).catch( (err) => { - log.error("Error getting user token ", err.Error); - throw err; - }); - } + public set_dm_room(discordId, discordChannel, roomId): Promise<null> { + log.silly("SQL", "set_dm_room => ", discordChannel); // Don't show discordId for privacy reasons + return this.db.Run( + ` + REPLACE INTO dm_rooms (discord_id,discord_channel,room_id) + VALUES ($discordId,$discordChannel,$roomId); + ` + , { + discordChannel, + discordId, + roomId, + }).catch( (err) => { + log.error("Error executing set_dm_room query ", err.Error); + throw err; + }); + } - public Get<T extends IDbData>(dbType: {new(): T; }, params: any): Promise<T|null> { - const dType = new dbType(); - log.silly(`get <${dType.constructor.name} with params ${params}>`); - return dType.RunQuery(this, params).then(() => { - log.silly(`Finished query with ${dType.Result ? "Results" : "No Results"}`); - return dType; - }).catch((ex) => { - log.warn(`get <${dType.constructor.name} with params ${params} FAILED with exception ${ex}>`); - return null; - }); - } + public get_all_user_discord_ids(): Promise<any> { + log.silly("SQL", "get_users_tokens"); + return this.db.All( + ` + SELECT * + FROM get_user_discord_ids + `, + ).then( (rows) => { + return rows; + }).catch( (err) => { + log.error("Error getting user token ", err.Error); + throw err; + }); + } - public Insert<T extends IDbData>(data: T): Promise<Error> { - log.silly(`insert <${data.constructor.name}>`); - return data.Insert(this); - } + public Get<T extends IDbData>(dbType: {new(): T; }, params: any): Promise<T|null> { + const dType = new dbType(); + log.silly(`get <${dType.constructor.name} with params ${params}>`); + return dType.RunQuery(this, params).then(() => { + log.silly(`Finished query with ${dType.Result ? "Results" : "No Results"}`); + return dType; + }).catch((ex) => { + log.warn(`get <${dType.constructor.name} with params ${params} FAILED with exception ${ex}>`); + return null; + }); + } - public Update<T extends IDbData>(data: T): Promise<Error> { - log.silly(`insert <${data.constructor.name}>`); - return data.Update(this); - } + public Insert<T extends IDbData>(data: T): Promise<Error> { + log.silly(`insert <${data.constructor.name}>`); + return data.Insert(this); + } - public Delete<T extends IDbData>(data: T): Promise<Error> { - log.silly(`insert <${data.constructor.name}>`); - return data.Delete(this); - } + public Update<T extends IDbData>(data: T): Promise<Error> { + log.silly(`insert <${data.constructor.name}>`); + return data.Update(this); + } - private async getSchemaVersion( ): Promise<number> { - log.silly("_get_schema_version"); - let version = 0; - try { - version = await this.db.Get(`SELECT version FROM schema`); - } catch (er) { - log.warn("Couldn't fetch schema version, defaulting to 0"); + public Delete<T extends IDbData>(data: T): Promise<Error> { + log.silly(`insert <${data.constructor.name}>`); + return data.Delete(this); } - return version; - } - private setSchemaVersion(ver: number): Promise<any> { - log.silly("_set_schema_version => ", ver); - return this.db.Run( - ` - UPDATE schema - SET version = $ver - `, {ver}, - ); - } + private async getSchemaVersion( ): Promise<number> { + log.silly("_get_schema_version"); + let version = 0; + try { + version = await this.db.Get(`SELECT version FROM schema`); + } catch (er) { + log.warn("Couldn't fetch schema version, defaulting to 0"); + } + return version; + } - private async open_database(): Promise<void|Error> { - if (this.config.filename) { - log.info("Filename present in config, using sqlite"); - this.db = new SQLite3(this.config.filename); - } else if (this.config.connString) { - log.info("connString present in config, using postgres"); - this.db = new Postgres(this.config.connString); + private setSchemaVersion(ver: number): Promise<any> { + log.silly("_set_schema_version => ", ver); + return this.db.Run( + ` + UPDATE schema + SET version = $ver + `, {ver}, + ); } - try { - this.db.Open(); - } catch (ex) { - log.error("Error opening database:", ex); - throw new Error("Couldn't open database. The appservice won't be able to continue."); + + private async open_database(): Promise<void|Error> { + if (this.config.filename) { + log.info("Filename present in config, using sqlite"); + this.db = new SQLite3(this.config.filename); + } else if (this.config.connString) { + log.info("connString present in config, using postgres"); + this.db = new Postgres(this.config.connString); + } + try { + this.db.Open(); + } catch (ex) { + log.error("Error opening database:", ex); + throw new Error("Couldn't open database. The appservice won't be able to continue."); + } } - } } diff --git a/src/usersyncroniser.ts b/src/usersyncroniser.ts index d91101abedfd800ba9824b2f75d7acb9169dd5b4..5724752ac19bf7799d16fc6bc7d73e1ed1047b68 100644 --- a/src/usersyncroniser.ts +++ b/src/usersyncroniser.ts @@ -1,37 +1,37 @@ -import {User, GuildMember, GuildChannel} from "discord.js"; +import { User, GuildMember, GuildChannel } from "discord.js"; import { DiscordBot } from "./bot"; -import {Util} from "./util"; +import { Util } from "./util"; import { MatrixUser, RemoteUser, Bridge, Entry, UserBridgeStore } from "matrix-appservice-bridge"; -import {DiscordBridgeConfig} from "./config"; +import { DiscordBridgeConfig } from "./config"; import * as Bluebird from "bluebird"; -import {Log} from "./log"; +import { Log } from "./log"; const log = new Log("UserSync"); const DEFAULT_USER_STATE = { - id: null, + avatarId: null, + avatarUrl: null, // Nullable createUser: false, - mxUserId: null, displayName: null, // Nullable - avatarUrl: null, // Nullable - avatarId: null, + id: null, + mxUserId: null, removeAvatar: false, }; const DEFAULT_GUILD_STATE = { + displayName: null, id: null, mxUserId: null, - displayName: null, roles: [], }; export interface IUserState { - id: string; + avatarId: string; + avatarUrl: string; // Nullable createUser: boolean; - mxUserId: string; displayName: string; // Nullable - avatarUrl: string; // Nullable - avatarId: string; + id: string; + mxUserId: string; removeAvatar: boolean; // If the avatar has been removed from the user. } @@ -42,9 +42,9 @@ export interface IGuildMemberRole { } export interface IGuildMemberState { + displayName: string; id: string; mxUserId: string; - displayName: string; roles: IGuildMemberRole[]; } @@ -154,9 +154,9 @@ export class UserSyncroniser { /* The intent class tries to be smart and deny a state update for <PL50 users. Obviously a user can change their own state so we use the client instead. */ const tryState = () => intent.getClient().sendStateEvent(roomId, "m.room.member", { - "membership": "join", "avatar_url": remoteUser.get("avatarurl_mxc"), "displayname": memberState.displayName, + "membership": "join", "uk.half-shot.discord.member": { id: memberState.id, roles: memberState.roles, @@ -228,8 +228,8 @@ export class UserSyncroniser { id: newMember.id, mxUserId: `@_discord_${newMember.id}:${this.config.bridge.domain}`, roles: newMember.roles.map((role) => { return { - name: role.name, color: role.color, + name: role.name, position: role.position, }; }), }); diff --git a/src/util.ts b/src/util.ts index ccc3b289e151d716ce06ee0a117321210f02fce6..c3f226a3ee4eb19194b0ec80a9992e093d5dc876 100644 --- a/src/util.ts +++ b/src/util.ts @@ -11,247 +11,246 @@ import { Log } from "./log"; const log = new Log("Util"); export interface ICommandAction { - params: string[]; - description?: string; - permission?: string; - run(params: any): Promise<any>; + params: string[]; + description?: string; + permission?: string; + run(params: any): Promise<any>; } export interface ICommandActions { - [index: string]: ICommandAction; + [index: string]: ICommandAction; } export interface ICommandParameter { - description?: string; - get(param: string): Promise<any>; + description?: string; + get(param: string): Promise<any>; } export interface ICommandParameters { - [index: string]: ICommandParameter; + [index: string]: ICommandParameter; } export class Util { + /** + * downloadFile - This function will take a URL and store the resulting data into + * a buffer. + */ + public static DownloadFile(url: string): Promise<Buffer> { + return new Promise((resolve, reject) => { + let ht; + if (url.startsWith("https")) { + ht = https; + } else { + ht = http; + } + const req = ht.get((url), (res) => { + let buffer = Buffer.alloc(0); + if (res.statusCode !== HTTP_OK) { + reject(`Non 200 status code (${res.statusCode})`); + } - /** - * downloadFile - This function will take a URL and store the resulting data into - * a buffer. - */ - public static DownloadFile(url: string): Promise<Buffer> { - return new Promise((resolve, reject) => { - let ht; - if (url.startsWith("https")) { - ht = https; - } else { - ht = http; - } - const req = ht.get((url), (res) => { - let buffer = Buffer.alloc(0); - if (res.statusCode !== HTTP_OK) { - reject(`Non 200 status code (${res.statusCode})`); - } + res.on("data", (d) => { + buffer = Buffer.concat([buffer, d]); + }); - res.on("data", (d) => { - buffer = Buffer.concat([buffer, d]); + res.on("end", () => { + resolve(buffer); + }); + }); + req.on("error", (err) => { + reject(`Failed to download. ${err.code}`); + }); }); + } + /** + * uploadContentFromUrl - Upload content from a given URL to the homeserver + * and return a MXC URL. + */ + public static UploadContentFromUrl(url: string, intent: Intent, name: string): Promise<IUploadResult> { + let contenttype; + let size; + name = name || null; + return new Promise((resolve, reject) => { + let ht; + if (url.startsWith("https")) { + ht = https; + } else { + ht = http; + } + const req = ht.get( url, (res) => { + let buffer = Buffer.alloc(0); - res.on("end", () => { - resolve(buffer); - }); - }); - req.on("error", (err) => { - reject(`Failed to download. ${err.code}`); - }); - }); - } - /** - * uploadContentFromUrl - Upload content from a given URL to the homeserver - * and return a MXC URL. - */ - public static UploadContentFromUrl(url: string, intent: Intent, name: string): Promise<IUploadResult> { - let contenttype; - let size; - name = name || null; - return new Promise((resolve, reject) => { - let ht; - if (url.startsWith("https")) { - ht = https; - } else { - ht = http; - } - const req = ht.get( url, (res) => { - let buffer = Buffer.alloc(0); + if (res.headers.hasOwnProperty("content-type")) { + contenttype = res.headers["content-type"]; + } else { + log.verbose("No content-type given by server, guessing based on file name."); + contenttype = mime.lookup(url); + } - if (res.headers.hasOwnProperty("content-type")) { - contenttype = res.headers["content-type"]; - } else { - log.verbose("No content-type given by server, guessing based on file name."); - contenttype = mime.lookup(url); - } + if (name === null) { + const names = url.split("/"); + name = names[names.length - 1]; + } - if (name === null) { - const names = url.split("/"); - name = names[names.length - 1]; - } + res.on("data", (d) => { + buffer = Buffer.concat([buffer, d]); + }); - res.on("data", (d) => { - buffer = Buffer.concat([buffer, d]); + res.on("end", () => { + resolve(buffer); + }); + }); + req.on("error", (err) => { + reject(`Failed to download. ${err.code}`); + }); + }).then((buffer: Buffer) => { + size = buffer.length; + return intent.getClient().uploadContent(buffer, { + name, + onlyContentUri: true, + rawResponse: false, + type: contenttype, + }); + }).then((contentUri) => { + log.verbose("Media uploaded to ", contentUri); + return { + mxcUrl: contentUri, + size, + }; + }).catch((reason) => { + log.error("Failed to upload content:\n", reason); + throw reason; }); + } - res.on("end", () => { - resolve(buffer); + /** + * Gets a promise that will resolve after the given number of milliseconds + * @param {number} duration The number of milliseconds to wait + * @returns {Promise<any>} The promise + */ + public static DelayedPromise(duration: number): Promise<any> { + return new Promise<any>((resolve, reject) => { + setTimeout(resolve, duration); }); - }); - req.on("error", (err) => { - reject(`Failed to download. ${err.code}`); - }); - }).then((buffer: Buffer) => { - size = buffer.length; - return intent.getClient().uploadContent(buffer, { - name, - type: contenttype, - onlyContentUri: true, - rawResponse: false, - }); - }).then((contentUri) => { - log.verbose("Media uploaded to ", contentUri); - return { - mxcUrl: contentUri, - size, - }; - }).catch((reason) => { - log.error("Failed to upload content:\n", reason); - throw reason; - }); - } - - /** - * Gets a promise that will resolve after the given number of milliseconds - * @param {number} duration The number of milliseconds to wait - * @returns {Promise<any>} The promise - */ - public static DelayedPromise(duration: number): Promise<any> { - return new Promise<any>((resolve, reject) => { - setTimeout(resolve, duration); - }); - } - - public static GetBotLink(config: any): string { - /* tslint:disable:no-bitwise */ - const perms = Permissions.FLAGS.READ_MESSAGES | - Permissions.FLAGS.SEND_MESSAGES | - Permissions.FLAGS.CHANGE_NICKNAME | - Permissions.FLAGS.CONNECT | - Permissions.FLAGS.SPEAK | - Permissions.FLAGS.EMBED_LINKS | - Permissions.FLAGS.ATTACH_FILES | - Permissions.FLAGS.READ_MESSAGE_HISTORY | - Permissions.FLAGS.MANAGE_WEBHOOKS | - Permissions.FLAGS.MANAGE_MESSAGES; - /* tslint:enable:no-bitwise */ + } - const clientId = config.auth.clientID; + public static GetBotLink(config: any): string { + /* tslint:disable:no-bitwise */ + const perms = Permissions.FLAGS.READ_MESSAGES | + Permissions.FLAGS.SEND_MESSAGES | + Permissions.FLAGS.CHANGE_NICKNAME | + Permissions.FLAGS.CONNECT | + Permissions.FLAGS.SPEAK | + Permissions.FLAGS.EMBED_LINKS | + Permissions.FLAGS.ATTACH_FILES | + Permissions.FLAGS.READ_MESSAGE_HISTORY | + Permissions.FLAGS.MANAGE_WEBHOOKS | + Permissions.FLAGS.MANAGE_MESSAGES; + /* tslint:enable:no-bitwise */ - return `https://discordapp.com/api/oauth2/authorize?client_id=${clientId}&scope=bot&permissions=${perms}`; - } + const clientId = config.auth.clientID; - public static async GetMxidFromName(intent: Intent, name: string, channelMxids: string[]) { - if (name[0] === "@" && name.includes(":")) { - return name; - } - const client = intent.getClient(); - const matrixUsers = {}; - let matches = 0; - await Promise.all(channelMxids.map((chan) => { - // we would use this.bridge.getBot().getJoinedMembers() - // but we also want to be able to search through banned members - // so we gotta roll our own thing - return client._http.authedRequestWithPrefix( - undefined, - "GET", - "/rooms/" + encodeURIComponent(chan) + "/members", - undefined, - undefined, - "/_matrix/client/r0", - ).then((res) => { - res.chunk.forEach((member) => { - if (member.membership !== "join" && member.membership !== "ban") { - return; - } - const mxid = member.state_key; - if (mxid.startsWith("@_discord_")) { - return; - } - let displayName = member.content.displayname; - if (!displayName && member.unsigned && member.unsigned.prev_content && - member.unsigned.prev_content.displayname) { - displayName = member.unsigned.prev_content.displayname; - } - if (!displayName) { - displayName = mxid.substring(1, mxid.indexOf(":")); - } - if (name.toLowerCase() === displayName.toLowerCase() || name === mxid) { - matrixUsers[mxid] = displayName; - matches++; - } - }); - }); - })); - if (matches === 0) { - throw Error(`No users matching ${name} found`); + return `https://discordapp.com/api/oauth2/authorize?client_id=${clientId}&scope=bot&permissions=${perms}`; } - if (matches > 1) { - let errStr = "Multiple matching users found:\n"; - for (const mxid of Object.keys(matrixUsers)) { - errStr += `${matrixUsers[mxid]} (\`${mxid}\`)\n`; - } - throw Error(errStr); - } - return Object.keys(matrixUsers)[0]; - } - public static async ParseCommand(action: ICommandAction, parameters: ICommandParameters, args: string[]) { - if (action.params.length === 1) { - args[0] = args.join(" "); - } - const params = {}; - let i = 0; - for (const param of action.params) { - params[param] = await parameters[param].get(args[i]); - i++; + public static async GetMxidFromName(intent: Intent, name: string, channelMxids: string[]) { + if (name[0] === "@" && name.includes(":")) { + return name; + } + const client = intent.getClient(); + const matrixUsers = {}; + let matches = 0; + await Promise.all(channelMxids.map((chan) => { + // we would use this.bridge.getBot().getJoinedMembers() + // but we also want to be able to search through banned members + // so we gotta roll our own thing + return client._http.authedRequestWithPrefix( + undefined, + "GET", + "/rooms/" + encodeURIComponent(chan) + "/members", + undefined, + undefined, + "/_matrix/client/r0", + ).then((res) => { + res.chunk.forEach((member) => { + if (member.membership !== "join" && member.membership !== "ban") { + return; + } + const mxid = member.state_key; + if (mxid.startsWith("@_discord_")) { + return; + } + let displayName = member.content.displayname; + if (!displayName && member.unsigned && member.unsigned.prev_content && + member.unsigned.prev_content.displayname) { + displayName = member.unsigned.prev_content.displayname; + } + if (!displayName) { + displayName = mxid.substring(1, mxid.indexOf(":")); + } + if (name.toLowerCase() === displayName.toLowerCase() || name === mxid) { + matrixUsers[mxid] = displayName; + matches++; + } + }); + }); + })); + if (matches === 0) { + throw Error(`No users matching ${name} found`); + } + if (matches > 1) { + let errStr = "Multiple matching users found:\n"; + for (const mxid of Object.keys(matrixUsers)) { + errStr += `${matrixUsers[mxid]} (\`${mxid}\`)\n`; + } + throw Error(errStr); + } + return Object.keys(matrixUsers)[0]; } - const retStr = await action.run(params); - return retStr; - } + public static async ParseCommand(action: ICommandAction, parameters: ICommandParameters, args: string[]) { + if (action.params.length === 1) { + args[0] = args.join(" "); + } + const params = {}; + let i = 0; + for (const param of action.params) { + params[param] = await parameters[param].get(args[i]); + i++; + } + + const retStr = await action.run(params); + return retStr; + } - public static MsgToArgs(msg: string, prefix: string) { - prefix += " "; - let command = "help"; - let args = []; - if (msg.length >= prefix.length) { - const allArgs = msg.substring(prefix.length).split(" "); - if (allArgs.length && allArgs[0] !== "") { - command = allArgs[0]; - allArgs.splice(0, 1); - args = allArgs; + public static MsgToArgs(msg: string, prefix: string) { + prefix += " "; + let command = "help"; + let args = []; + if (msg.length >= prefix.length) { + const allArgs = msg.substring(prefix.length).split(" "); + if (allArgs.length && allArgs[0] !== "") { + command = allArgs[0]; + allArgs.splice(0, 1); + args = allArgs; + } } + return {command, args}; } - return {command, args}; - } - public static GetReplyFromReplyBody(body: string) { - const lines = body.split("\n"); - while (lines[0].startsWith("> ") || lines[0].trim().length === 0) { - lines.splice(0, 1); - if (lines.length === 0) { - return ""; - } - } - return lines.join("\n").trim(); - } + public static GetReplyFromReplyBody(body: string) { + const lines = body.split("\n"); + while (lines[0].startsWith("> ") || lines[0].trim().length === 0) { + lines.splice(0, 1); + if (lines.length === 0) { + return ""; + } + } + return lines.join("\n").trim(); + } } interface IUploadResult { - mxcUrl: string; - size: number; + mxcUrl: string; + size: number; } diff --git a/test/config.ts b/test/config.ts index cba8b315bc53f11a180327733e313476f7976ea0..12cd897f25bf3240306be2559b30a9271a3ed35f 100644 --- a/test/config.ts +++ b/test/config.ts @@ -1,6 +1,9 @@ import {argv} from "process"; import {Log} from "../src/log"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count */ + if (!argv.includes("--noisy")) { Log.ForceSilent(); } diff --git a/test/mocks/collection.ts b/test/mocks/collection.ts index c02d9c8055a072ae436ea974c57c8b81168c8d08..1dd67428eaca64ac15e9374d23923d953dca57f6 100644 --- a/test/mocks/collection.ts +++ b/test/mocks/collection.ts @@ -1,11 +1,11 @@ import { Collection } from "discord.js"; export class MockCollection<T1, T2> extends Collection<T1, T2> { - public array(): T2[] { - return [...this.values()]; - } + public array(): T2[] { + return [...this.values()]; + } - public keyArray(): T1[] { - return [...this.keys()]; - } + public keyArray(): T1[] { + return [...this.keys()]; + } } diff --git a/test/mocks/discordclient.ts b/test/mocks/discordclient.ts index 20da7ba2e72ef8d2dcbb5b801e2b5acfcce10b44..21e152f4d400d03c24a42639022dff5f3f8cf85f 100644 --- a/test/mocks/discordclient.ts +++ b/test/mocks/discordclient.ts @@ -4,51 +4,53 @@ import {MockUser} from "./user"; import { EventEmitter } from "events"; export class MockDiscordClient { - public guilds = new MockCollection<string, MockGuild>(); - public user: MockUser; - private testLoggedIn: boolean = false; - private testCallbacks: Map<string, (...data: any[]) => void> = new Map(); + public guilds = new MockCollection<string, MockGuild>(); + public user: MockUser; + private testLoggedIn: boolean = false; + private testCallbacks: Map<string, (...data: any[]) => void> = new Map(); - constructor() { - const channels = [ - { - id: "321", - name: "achannel", - type: "text", - }, { - id: "654", - name: "a-channel", - type: "text", - }, { - id: "987", - name: "a channel", - type: "text", - }, - ]; - this.guilds.set("123", new MockGuild("MyGuild", channels)); - this.guilds.set("456", new MockGuild("My Spaces Gui", channels)); - this.guilds.set("789", new MockGuild("My Dash-Guild", channels)); - this.user = new MockUser("12345"); - } - - public on(event: string, callback: (...data: any[]) => void) { - this.testCallbacks.set(event, callback); - } + constructor() { + const channels = [ + { + id: "321", + name: "achannel", + type: "text", + }, + { + id: "654", + name: "a-channel", + type: "text", + }, + { + id: "987", + name: "a channel", + type: "text", + }, + ]; + this.guilds.set("123", new MockGuild("MyGuild", channels)); + this.guilds.set("456", new MockGuild("My Spaces Gui", channels)); + this.guilds.set("789", new MockGuild("My Dash-Guild", channels)); + this.user = new MockUser("12345"); + } - public emit(event: string, ...data: any[]) { - return this.testCallbacks.get(event).apply(this, data); - } + public on(event: string, callback: (...data: any[]) => void) { + this.testCallbacks.set(event, callback); + } - public async login(token: string): Promise<void> { - if (token !== "passme") { - throw new Error("Mock Discord Client only logins with the token 'passme'"); + public emit(event: string, ...data: any[]) { + return this.testCallbacks.get(event).apply(this, data); } - this.testLoggedIn = true; - if (this.testCallbacks.has("ready")) { - this.testCallbacks.get("ready")(); + + public async login(token: string): Promise<void> { + if (token !== "passme") { + throw new Error("Mock Discord Client only logins with the token 'passme'"); + } + this.testLoggedIn = true; + if (this.testCallbacks.has("ready")) { + this.testCallbacks.get("ready")(); + } + return; } - return; - } - public destroy() { } // no-op + public destroy() { } // no-op } diff --git a/test/mocks/discordclientfactory.ts b/test/mocks/discordclientfactory.ts index 489adba780b375bbdae808026258095a0a089ef0..a09e08e0d70f76da480f77ef0a3058977d3ed7e6 100644 --- a/test/mocks/discordclientfactory.ts +++ b/test/mocks/discordclientfactory.ts @@ -1,19 +1,19 @@ import {MockDiscordClient} from "./discordclient"; export class DiscordClientFactory { - private botClient: MockDiscordClient = null; - constructor(config: any, store: any) { - - } + private botClient: MockDiscordClient = null; + constructor(config: any, store: any) { - public init(): Promise<void> { - return Promise.resolve(); - } + } + + public init(): Promise<void> { + return Promise.resolve(); + } - public getClient(userId?: string): Promise<MockDiscordClient> { - if (userId == null && !this.botClient) { - this.botClient = new MockDiscordClient(); + public getClient(userId?: string): Promise<MockDiscordClient> { + if (userId == null && !this.botClient) { + this.botClient = new MockDiscordClient(); + } + return Promise.resolve(this.botClient); } - return Promise.resolve(this.botClient); - } } diff --git a/test/mocks/emoji.ts b/test/mocks/emoji.ts index c02734bdcce767512d9b2df8d2bb77b2830efd48..222db143abfbfc98124024714fc81771517b6dd2 100644 --- a/test/mocks/emoji.ts +++ b/test/mocks/emoji.ts @@ -1,3 +1,3 @@ export class MockEmoji { - constructor(public id: string = "", public name = "") { } + constructor(public id: string = "", public name = "") { } } diff --git a/test/mocks/guild.ts b/test/mocks/guild.ts index bc0b625c05255619fad2ea3847a1036d6378ff20..7365691a0618fe54cf374e804143559ea6175bde 100644 --- a/test/mocks/guild.ts +++ b/test/mocks/guild.ts @@ -4,28 +4,28 @@ import {MockEmoji} from "./emoji"; import {Channel} from "discord.js"; export class MockGuild { - public channels = new MockCollection<string, Channel>(); - public members = new MockCollection<string, MockMember>(); - public emojis = new MockCollection<string, MockEmoji>(); - public id: string; - public name: string; - public icon: string; - constructor(id: string, channels: any[] = [], name: string = null) { - this.id = id; - this.name = name || id; - channels.forEach((item) => { - this.channels.set(item.id, item); - }); - } + public channels = new MockCollection<string, Channel>(); + public members = new MockCollection<string, MockMember>(); + public emojis = new MockCollection<string, MockEmoji>(); + public id: string; + public name: string; + public icon: string; + constructor(id: string, channels: any[] = [], name: string = null) { + this.id = id; + this.name = name || id; + channels.forEach((item) => { + this.channels.set(item.id, item); + }); + } - public fetchMember(id: string): Promise<MockMember|Error> { - if (this.members.has(id)) { - return Promise.resolve(this.members.get(id)); + public fetchMember(id: string): Promise<MockMember|Error> { + if (this.members.has(id)) { + return Promise.resolve(this.members.get(id)); + } + return Promise.reject("Member not in this guild"); } - return Promise.reject("Member not in this guild"); - } - public _mockAddMember(member: MockMember) { - this.members.set(member.id, member); - } + public _mockAddMember(member: MockMember) { + this.members.set(member.id, member); + } } diff --git a/test/mocks/member.ts b/test/mocks/member.ts index 6ddddb4db1aa75c39d429170c07193e92debc0cd..b1fef212bf4acdd9ce057877043ad5916fea85ff 100644 --- a/test/mocks/member.ts +++ b/test/mocks/member.ts @@ -2,19 +2,19 @@ import {MockUser} from "./user"; import * as Discord from "discord.js"; export class MockMember { - public id = ""; - public presence: Discord.Presence; - public user: MockUser; - public nickname: string; - public roles = []; - constructor(id: string, username: string, public guild = null, public displayName: string = username) { - this.id = id; - this.presence = new Discord.Presence({}); - this.user = new MockUser(this.id, username); - this.nickname = displayName; - } + public id = ""; + public presence: Discord.Presence; + public user: MockUser; + public nickname: string; + public roles = []; + constructor(id: string, username: string, public guild = null, public displayName: string = username) { + this.id = id; + this.presence = new Discord.Presence({}); + this.user = new MockUser(this.id, username); + this.nickname = displayName; + } - public MockSetPresence(presence: Discord.Presence) { - this.presence = presence; - } + public MockSetPresence(presence: Discord.Presence) { + this.presence = presence; + } } diff --git a/test/mocks/user.ts b/test/mocks/user.ts index d4545e1b2849fa774e3e51cfbe9a98e35240df7c..43d5929a241b531cb4015e466a49541e60cbe029 100644 --- a/test/mocks/user.ts +++ b/test/mocks/user.ts @@ -1,16 +1,16 @@ import { Presence } from "discord.js"; export class MockUser { - public presence: Presence; - constructor( - public id: string, - public username: string = "", - public discriminator: string = "", - public avatarURL: string = "", - public avatar: string = "", - ) { } + public presence: Presence; + constructor( + public id: string, + public username: string = "", + public discriminator: string = "", + public avatarURL: string = "", + public avatar: string = "", + ) { } - public MockSetPresence(presence: Presence) { - this.presence = presence; - } + public MockSetPresence(presence: Presence) { + this.presence = presence; + } } diff --git a/test/test_channelsyncroniser.ts b/test/test_channelsyncroniser.ts index 01b7aabcd1c83e48f4355e8a3056c9f553e4c4b4..a3e91a975146dc1fca4b746044c06c90be4041a1 100644 --- a/test/test_channelsyncroniser.ts +++ b/test/test_channelsyncroniser.ts @@ -13,6 +13,9 @@ import { MessageProcessor, MessageProcessorOpts } from "../src/messageprocessor" import { MockChannel } from "./mocks/channel"; import { Bridge, MatrixRoom, RemoteRoom } from "matrix-appservice-bridge"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -52,10 +55,69 @@ class Entry { function CreateChannelSync(remoteChannels: any[] = []): ChannelSyncroniser { UTIL_UPLOADED_AVATAR = false; const bridge: any = { + getIntent: (id) => { + ROOM_NAME_SET = null; + ROOM_TOPIC_SET = null; + ROOM_AVATAR_SET = null; + STATE_EVENT_SENT = false; + ALIAS_DELETED = false; + ROOM_DIRECTORY_VISIBILITY = null; + return { + getClient: () => { + return { + deleteAlias: (alias) => { + ALIAS_DELETED = true; + return Promise.resolve(); + }, + getStateEvent: (mxid, event) => { + return Promise.resolve(event); + }, + sendStateEvent: (mxid, event, data) => { + STATE_EVENT_SENT = true; + return Promise.resolve(); + }, + setRoomDirectoryVisibility: (mxid, visibility) => { + ROOM_DIRECTORY_VISIBILITY = visibility; + return Promise.resolve(); + }, + setRoomName: (mxid, name) => { + ROOM_NAME_SET = name; + return Promise.resolve(); + }, + setRoomTopic: (mxid, topic) => { + ROOM_TOPIC_SET = topic; + return Promise.resolve(); + }, + }; + }, + setRoomAvatar: (mxid, mxc) => { + ROOM_AVATAR_SET = mxc; + return Promise.resolve(); + }, + setRoomName: (mxid, name) => { + ROOM_NAME_SET = name; + return Promise.resolve(); + }, + setRoomTopic: (mxid, topic) => { + ROOM_TOPIC_SET = topic; + return Promise.resolve(); + }, + }; + }, getRoomStore: () => { REMOTECHANNEL_SET = false; REMOTECHANNEL_REMOVED = false; return { + getEntriesByMatrixId: (roomid) => { + const entries = []; + remoteChannels.forEach((c) => { + const mxid = c.matrix.getId(); + if (roomid === mxid) { + entries.push(c); + } + }); + return entries; + }, getEntriesByMatrixIds: (roomids) => { const entries = {}; remoteChannels.forEach((c) => { @@ -69,16 +131,6 @@ function CreateChannelSync(remoteChannels: any[] = []): ChannelSyncroniser { }); return entries; }, - getEntriesByMatrixId: (roomid) => { - const entries = []; - remoteChannels.forEach((c) => { - const mxid = c.matrix.getId(); - if (roomid === mxid) { - entries.push(c); - } - }); - return entries; - }, getEntriesByRemoteRoomData: (data) => { return remoteChannels.filter((c) => { for (const d of Object.keys(data)) { @@ -89,60 +141,11 @@ function CreateChannelSync(remoteChannels: any[] = []): ChannelSyncroniser { return true; }); }, - upsertEntry: (room) => { - REMOTECHANNEL_SET = true; - }, removeEntriesByMatrixRoomId: (room) => { REMOTECHANNEL_REMOVED = true; }, - }; - }, - getIntent: (id) => { - ROOM_NAME_SET = null; - ROOM_TOPIC_SET = null; - ROOM_AVATAR_SET = null; - STATE_EVENT_SENT = false; - ALIAS_DELETED = false; - ROOM_DIRECTORY_VISIBILITY = null; - return { - setRoomName: (mxid, name) => { - ROOM_NAME_SET = name; - return Promise.resolve(); - }, - setRoomTopic: (mxid, topic) => { - ROOM_TOPIC_SET = topic; - return Promise.resolve(); - }, - setRoomAvatar: (mxid, mxc) => { - ROOM_AVATAR_SET = mxc; - return Promise.resolve(); - }, - getClient: () => { - return { - getStateEvent: (mxid, event) => { - return Promise.resolve(event); - }, - setRoomName: (mxid, name) => { - ROOM_NAME_SET = name; - return Promise.resolve(); - }, - setRoomTopic: (mxid, topic) => { - ROOM_TOPIC_SET = topic; - return Promise.resolve(); - }, - sendStateEvent: (mxid, event, data) => { - STATE_EVENT_SENT = true; - return Promise.resolve(); - }, - deleteAlias: (alias) => { - ALIAS_DELETED = true; - return Promise.resolve(); - }, - setRoomDirectoryVisibility: (mxid, visibility) => { - ROOM_DIRECTORY_VISIBILITY = visibility; - return Promise.resolve(); - }, - }; + upsertEntry: (room) => { + REMOTECHANNEL_SET = true; }, }; }, @@ -166,10 +169,10 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, }, + remote_id: "111", }), ]; @@ -186,10 +189,10 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, }, + remote_id: "111", }), ]; @@ -207,10 +210,10 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, }, + remote_id: "111", }), ]; @@ -227,26 +230,26 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, }, + remote_id: "111", }), new Entry({ id: "2", matrix_id: "!2:localhost", - remote_id: "111", remote: { discord_channel: chan.id, }, + remote_id: "111", }), new Entry({ id: "3", matrix_id: "!3:localhost", - remote_id: "false", remote: { discord_channel: "no", }, + remote_id: "false", }), ]; @@ -291,7 +294,6 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, discord_name: "[Discord] oldGuild oldName", @@ -299,6 +301,7 @@ describe("ChannelSyncroniser", () => { update_name: true, update_topic: true, }, + remote_id: "111", }), ]; @@ -322,12 +325,12 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, discord_name: "[Discord] oldGuild oldName", discord_topic: "oldTopic", }, + remote_id: "111", }), ]; @@ -351,7 +354,6 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, discord_name: "[Discord] newGuild #newName", @@ -359,6 +361,7 @@ describe("ChannelSyncroniser", () => { update_name: true, update_topic: true, }, + remote_id: "111", }), ]; @@ -381,12 +384,12 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, discord_iconurl: "https://cdn.discordapp.com/icons/654321/old_icon.png", update_icon: true, }, + remote_id: "111", }), ]; @@ -409,12 +412,12 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, discord_iconurl: "https://cdn.discordapp.com/icons/654321/new_icon.png", update_icon: true, }, + remote_id: "111", }), ]; @@ -437,12 +440,12 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, discord_iconurl: "https://cdn.discordapp.com/icons/654321/icon.png", update_icon: true, }, + remote_id: "111", }), ]; @@ -468,16 +471,16 @@ describe("ChannelSyncroniser", () => { new Entry({ id: "1", matrix_id: "!1:localhost", - remote_id: "111", remote: { discord_channel: chan.id, + discord_iconurl: "https://cdn.discordapp.com/icons/654321/old_icon.png", discord_name: "[Discord] oldGuild #oldName", discord_topic: "oldTopic", - discord_iconurl: "https://cdn.discordapp.com/icons/654321/old_icon.png", + update_icon: true, update_name: true, update_topic: true, - update_icon: true, }, + remote_id: "111", }), ]; diff --git a/test/test_clientfactory.ts b/test/test_clientfactory.ts index 6660d4cc2f6246ad74c180fcf7fb05d4b784c8a4..2d48b64ad48cc00e7f2356be33929432a8709099 100644 --- a/test/test_clientfactory.ts +++ b/test/test_clientfactory.ts @@ -4,6 +4,9 @@ import * as Proxyquire from "proxyquire"; import {DiscordBridgeConfigAuth} from "../src/config"; import {MockDiscordClient} from "./mocks/discordclient"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -12,14 +15,6 @@ const DiscordClientFactory = Proxyquire("../src/clientfactory", { }).DiscordClientFactory; const STORE = { - get_user_discord_ids: (userid: string) => { - if (userid === "@valid:localhost") { - return Promise.resolve(["12345"]); - } else if (userid === "@invalid:localhost") { - return Promise.resolve(["1234555"]); - } - return Promise.resolve([]); - }, get_token: (discordid: string) => { if (discordid === "12345") { return Promise.resolve("passme"); @@ -28,6 +23,14 @@ const STORE = { } return Promise.reject("Token not found"); }, + get_user_discord_ids: (userid: string) => { + if (userid === "@valid:localhost") { + return Promise.resolve(["12345"]); + } else if (userid === "@invalid:localhost") { + return Promise.resolve(["1234555"]); + } + return Promise.resolve([]); + }, }; describe("ClientFactory", () => { diff --git a/test/test_config.ts b/test/test_config.ts index 32c305699d56257e9e208912cdbc368434cc5cea..472d944e11772daadd479b3bba5961437b2c1f42 100644 --- a/test/test_config.ts +++ b/test/test_config.ts @@ -1,6 +1,10 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import { DiscordBridgeConfig } from "../src/config"; + +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -9,11 +13,11 @@ describe("DiscordBridgeConfig.ApplyConfig", () => { const config = new DiscordBridgeConfig(); config.ApplyConfig({ bridge: { - homeserverUrl: "blah", - disableTypingNotifications: true, - disableDiscordMentions: false, disableDeletionForwarding: true, + disableDiscordMentions: false, + disableTypingNotifications: true, enableSelfServiceBridging: false, + homeserverUrl: "blah", }, logging: { console: "warn", diff --git a/test/test_configschema.ts b/test/test_configschema.ts index 0533016160c9240d477a1ec8db00d71afc17ae05..439ba3c415db72c85c55f8ed10801866b2cba59a 100644 --- a/test/test_configschema.ts +++ b/test/test_configschema.ts @@ -2,6 +2,9 @@ import * as yaml from "js-yaml"; import * as Chai from "chai"; import { ConfigValidator } from "matrix-appservice-bridge"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + const expect = Chai.expect; describe("ConfigSchema", () => { diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts index a350b1784949c320af1cdf1fa9c093b155ca23ae..704394777bf2c576bec6cc5dc35238ffbb0e259d 100644 --- a/test/test_discordbot.ts +++ b/test/test_discordbot.ts @@ -10,192 +10,195 @@ import { MockMember } from "./mocks/member"; import { DiscordBot } from "../src/bot"; import { MockDiscordClient } from "./mocks/discordclient"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const assert = Chai.assert; // const should = Chai.should as any; const mockBridge = { - getRoomStore: () => { - return { - getEntriesByRemoteRoomData: (data) => { - if (data.discord_channel === "321") { - return Promise.resolve([{ - matrix: { - getId: () => "foobar:example.com", + getIntentFromLocalpart: (localpart: string) => { + return { + sendTyping: (room: string, isTyping: boolean) => { + return; }, - }]); - } - return Promise.resolve([]); - }, - }; - }, - getIntentFromLocalpart: (localpart: string) => { - return { - sendTyping: (room: string, isTyping: boolean) => { - return; - }, - }; - }, - getUserStore: () => { - return {}; - }, + }; + }, + getRoomStore: () => { + return { + getEntriesByRemoteRoomData: (data) => { + if (data.discord_channel === "321") { + return Promise.resolve([{ + matrix: { + getId: () => "foobar:example.com", + }, + }]); + } + return Promise.resolve([]); + }, + }; + }, + getUserStore: () => { + return {}; + }, }; const modDiscordBot = Proxyquire("../src/bot", { - "./clientfactory": require("./mocks/discordclientfactory"), + "./clientfactory": require("./mocks/discordclientfactory"), }); describe("DiscordBot", () => { - let discordBot; - const config = { - auth: { - botToken: "blah", - }, - bridge: { - domain: "localhost", - }, - limits: { - discordSendDelay: 50, - }, - }; - describe("run()", () => { - it("should resolve when ready.", () => { - discordBot = new modDiscordBot.DiscordBot( - config, - null, - ); - discordBot.setBridge(mockBridge); - return discordBot.run(); - }); - }); - - describe("LookupRoom()", () => { - beforeEach(() => { - discordBot = new modDiscordBot.DiscordBot( - config, - null, - ); - discordBot.setBridge(mockBridge); - return discordBot.run(); - }); - it("should reject a missing guild.", () => { - return assert.isRejected(discordBot.LookupRoom("541", "321")); + let discordBot; + const config = { + auth: { + botToken: "blah", + }, + bridge: { + domain: "localhost", + }, + limits: { + discordSendDelay: 50, + }, + }; + describe("run()", () => { + it("should resolve when ready.", () => { + discordBot = new modDiscordBot.DiscordBot( + config, + null, + ); + discordBot.setBridge(mockBridge); + return discordBot.run(); + }); }); - it("should reject a missing channel.", () => { - return assert.isRejected(discordBot.LookupRoom("123", "666")); + describe("LookupRoom()", () => { + beforeEach(() => { + discordBot = new modDiscordBot.DiscordBot( + config, + null, + ); + discordBot.setBridge(mockBridge); + return discordBot.run(); + }); + it("should reject a missing guild.", () => { + return assert.isRejected(discordBot.LookupRoom("541", "321")); + }); + + it("should reject a missing channel.", () => { + return assert.isRejected(discordBot.LookupRoom("123", "666")); + }); + + it("should resolve a guild and channel id.", () => { + return assert.isFulfilled(discordBot.LookupRoom("123", "321")); + }); }); - - it("should resolve a guild and channel id.", () => { - return assert.isFulfilled(discordBot.LookupRoom("123", "321")); + describe("OnMessageUpdate()", () => { + it("should return on an unchanged message", () => { + discordBot = new modDiscordBot.DiscordBot( + config, + mockBridge, + ); + + const guild: any = new MockGuild("123", []); + guild._mockAddMember(new MockMember("12345", "TestUsername")); + const channel = new Discord.TextChannel(guild, null); + const oldMsg = new Discord.Message(channel, null, null); + const newMsg = new Discord.Message(channel, null, null); + oldMsg.embeds = []; + newMsg.embeds = []; + + // Content updated but not changed + oldMsg.content = "a"; + newMsg.content = "a"; + + // Mock the SendMatrixMessage method to check if it is called + let checkMsgSent = false; + discordBot.SendMatrixMessage = (...args) => checkMsgSent = true; + + discordBot.OnMessageUpdate(oldMsg, newMsg).then(() => { + Chai.assert.equal(checkMsgSent, false); + }); + }); + + it("should send a matrix message on an edited discord message", () => { + discordBot = new modDiscordBot.DiscordBot( + config, + mockBridge, + ); + + const guild: any = new MockGuild("123", []); + guild._mockAddMember(new MockMember("12345", "TestUsername")); + const channel = new Discord.TextChannel(guild, null); + const oldMsg = new Discord.Message(channel, null, null); + const newMsg = new Discord.Message(channel, null, null); + oldMsg.embeds = []; + newMsg.embeds = []; + + // Content updated and edited + oldMsg.content = "a"; + newMsg.content = "b"; + + // Mock the SendMatrixMessage method to check if it is called + let checkMsgSent = false; + discordBot.SendMatrixMessage = (...args) => checkMsgSent = true; + + discordBot.OnMessageUpdate(oldMsg, newMsg).then(() => { + Chai.assert.equal(checkMsgSent, true); + }); + }); }); - }); - describe("OnMessageUpdate()", () => { - it("should return on an unchanged message", () => { - discordBot = new modDiscordBot.DiscordBot( - config, - mockBridge, - ); - - const guild: any = new MockGuild("123", []); - guild._mockAddMember(new MockMember("12345", "TestUsername")); - const channel = new Discord.TextChannel(guild, null); - const oldMsg = new Discord.Message(channel, null, null); - const newMsg = new Discord.Message(channel, null, null); - oldMsg.embeds = []; - newMsg.embeds = []; - - // Content updated but not changed - oldMsg.content = "a"; - newMsg.content = "a"; - - // Mock the SendMatrixMessage method to check if it is called - let checkMsgSent = false; - discordBot.SendMatrixMessage = (...args) => checkMsgSent = true; - - discordBot.OnMessageUpdate(oldMsg, newMsg).then(() => { - Chai.assert.equal(checkMsgSent, false); - }); + describe("event:message", () => { + it("should delay messages so they arrive in order", async () => { + discordBot = new modDiscordBot.DiscordBot( + config, + mockBridge, + ); + let expected = 0; + discordBot.OnMessage = (msg: any) => { + assert.equal(msg.n, expected); + expected++; + return Promise.resolve(); + }; + const client: MockDiscordClient = (await discordBot.ClientFactory.getClient()) as MockDiscordClient; + discordBot.setBridge(mockBridge); + await discordBot.run(); + const ITERATIONS = 25; + const CHANID = 123; + // Send delay of 50ms, 2 seconds / 50ms - 5 for safety. + for (let i = 0; i < ITERATIONS; i++) { + client.emit("message", { n: i, channel: { id: CHANID} }); + } + await discordBot.discordMessageQueue[CHANID]; + }); }); - it("should send a matrix message on an edited discord message", () => { - discordBot = new modDiscordBot.DiscordBot( - config, - mockBridge, - ); - - const guild: any = new MockGuild("123", []); - guild._mockAddMember(new MockMember("12345", "TestUsername")); - const channel = new Discord.TextChannel(guild, null); - const oldMsg = new Discord.Message(channel, null, null); - const newMsg = new Discord.Message(channel, null, null); - oldMsg.embeds = []; - newMsg.embeds = []; - - // Content updated and edited - oldMsg.content = "a"; - newMsg.content = "b"; - - // Mock the SendMatrixMessage method to check if it is called - let checkMsgSent = false; - discordBot.SendMatrixMessage = (...args) => checkMsgSent = true; - - discordBot.OnMessageUpdate(oldMsg, newMsg).then(() => { - Chai.assert.equal(checkMsgSent, true); - }); - }); - }); - describe("event:message", () => { - it("should delay messages so they arrive in order", async () => { - discordBot = new modDiscordBot.DiscordBot( - config, - mockBridge, - ); - let expected = 0; - discordBot.OnMessage = (msg: any) => { - assert.equal(msg.n, expected); - expected++; - return Promise.resolve(); - }; - const client: MockDiscordClient = (await discordBot.ClientFactory.getClient()) as MockDiscordClient; - discordBot.setBridge(mockBridge); - await discordBot.run(); - const ITERATIONS = 25; - const CHANID = 123; - // Send delay of 50ms, 2 seconds / 50ms - 5 for safety. - for (let i = 0; i < ITERATIONS; i++) { - client.emit("message", { n: i, channel: { id: CHANID} }); - } - await discordBot.discordMessageQueue[CHANID]; - }); - }); - - // describe("ProcessMatrixMsgEvent()", () => { - // - // }); - // describe("UpdateRoom()", () => { - // - // }); - // describe("UpdateUser()", () => { - // - // }); - // describe("UpdatePresence()", () => { - // - // }); - // describe("OnTyping()", () => { - // const discordBot = new modDiscordBot.DiscordBot( - // config, - // ); - // discordBot.setBridge(mockBridge); - // discordBot.run(); - // it("should reject an unknown room.", () => { - // return assert.isRejected(discordBot.OnTyping( {id: "512"}, {id: "12345"}, true)); - // }); - // it("should resolve a known room.", () => { - // return assert.isFulfilled(discordBot.OnTyping( {id: "321"}, {id: "12345"}, true)); - // }); - // }); - // describe("OnMessage()", () => { - // - // }); + // describe("ProcessMatrixMsgEvent()", () => { + // + // }); + // describe("UpdateRoom()", () => { + // + // }); + // describe("UpdateUser()", () => { + // + // }); + // describe("UpdatePresence()", () => { + // + // }); + // describe("OnTyping()", () => { + // const discordBot = new modDiscordBot.DiscordBot( + // config, + // ); + // discordBot.setBridge(mockBridge); + // discordBot.run(); + // it("should reject an unknown room.", () => { + // return assert.isRejected(discordBot.OnTyping( {id: "512"}, {id: "12345"}, true)); + // }); + // it("should resolve a known room.", () => { + // return assert.isFulfilled(discordBot.OnTyping( {id: "321"}, {id: "12345"}, true)); + // }); + // }); + // describe("OnMessage()", () => { + // + // }); }); diff --git a/test/test_log.ts b/test/test_log.ts index c70e6fef896280a06053e4766638db816b9e3de2..42a7f9c787f1d004b74aa4d50d77bcf70f382808 100644 --- a/test/test_log.ts +++ b/test/test_log.ts @@ -2,6 +2,10 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import * as Proxyquire from "proxyquire"; import * as RealLog from "../src/log"; + +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -12,15 +16,15 @@ let loggedMessages = []; const WinstonMock = { createLogger: (format, transports) => { return createdLogger = { - format, - transports, close: () => { loggerClosed = true; }, - silent: false, + format, log: (type, ...msg) => { loggedMessages = loggedMessages.concat(msg); }, + silent: false, + transports, }; }, }; diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts index beb033672d8afc01c5ed81dfeeee336110f7201e..861e164b77862a5a7c966191454da9cb96d30aa9 100644 --- a/test/test_matrixeventprocessor.ts +++ b/test/test_matrixeventprocessor.ts @@ -14,6 +14,9 @@ import {DiscordBridgeConfig} from "../src/config"; import {MessageProcessor, MessageProcessorOpts} from "../src/messageprocessor"; import {MockChannel} from "./mocks/channel"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; // const assert = Chai.assert; @@ -35,9 +38,17 @@ const mxClient = { }, }; -function createMatrixEventProcessor - (disableMentions: boolean = false, disableEveryone = false, disableHere = false): MatrixEventProcessor { +function createMatrixEventProcessor( + disableMentions: boolean = false, + disableEveryone = false, + disableHere = false, +): MatrixEventProcessor { const bridge = { + getBot: () => { + return { + isRemoteUser: () => false, + }; + }, getClientFactory: () => { return { getClientAs: () => { @@ -45,11 +56,6 @@ function createMatrixEventProcessor }, }; }, - getBot: () => { - return { - isRemoteUser: () => false, - }; - }, getIntent: () => { return { getClient: () => { @@ -62,31 +68,31 @@ function createMatrixEventProcessor getEvent: async (_, eventId: string) => { if (eventId === "$goodEvent:localhost") { return { - sender: "@doggo:localhost", content: { body: "Hello!", }, + sender: "@doggo:localhost", }; } else if (eventId === "$reply:localhost") { return { - sender: "@doggo:localhost", content: { "body": `> <@doggo:localhost> This is the original body -This is the first reply`, + This is the first reply`, "m.relates_to": { "m.in_reply_to": { event_id: "$goodEvent:localhost", }, }, }, + sender: "@doggo:localhost", }; } else if (eventId === "$nontext:localhost") { return { - sender: "@doggo:localhost", content: { something: "not texty", }, + sender: "@doggo:localhost", }; } return null; @@ -96,8 +102,8 @@ This is the first reply`, return null; } return { - displayname: "Doggo!", avatar_url: "mxc://fakeurl.com", + displayname: "Doggo!", }; }, }; @@ -154,11 +160,11 @@ describe("MatrixEventProcessor", () => { it("Should echo name changes", () => { const processor = createMatrixEventProcessor(); const event = { - sender: "@user:localhost", - type: "m.room.name", content: { name: "Test Name", }, + sender: "@user:localhost", + type: "m.room.name", }; const channel = new MockChannel("123456"); const msg = processor.StateEventToMessage(event, channel as any); @@ -167,11 +173,11 @@ describe("MatrixEventProcessor", () => { it("Should echo topic changes", () => { const processor = createMatrixEventProcessor(); const event = { - sender: "@user:localhost", - type: "m.room.topic", content: { topic: "Test Topic", }, + sender: "@user:localhost", + type: "m.room.topic", }; const channel = new MockChannel("123456"); const msg = processor.StateEventToMessage(event, channel as any); @@ -180,11 +186,11 @@ describe("MatrixEventProcessor", () => { it("Should echo joins", () => { const processor = createMatrixEventProcessor(); const event = { - sender: "@user:localhost", - type: "m.room.member", content: { membership: "join", }, + sender: "@user:localhost", + type: "m.room.member", unsigned: {}, }; const channel = new MockChannel("123456"); @@ -194,13 +200,13 @@ describe("MatrixEventProcessor", () => { it("Should echo invites", () => { const processor = createMatrixEventProcessor(); const event = { - sender: "@user:localhost", - type: "m.room.member", content: { membership: "invite", }, - unsigned: {}, + sender: "@user:localhost", state_key: "@user2:localhost", + type: "m.room.member", + unsigned: {}, }; const channel = new MockChannel("123456"); const msg = processor.StateEventToMessage(event, channel as any); @@ -209,13 +215,13 @@ describe("MatrixEventProcessor", () => { it("Should echo kicks", () => { const processor = createMatrixEventProcessor(); const event = { - sender: "@user:localhost", - type: "m.room.member", content: { membership: "leave", }, - unsigned: {}, + sender: "@user:localhost", state_key: "@user2:localhost", + type: "m.room.member", + unsigned: {}, }; const channel = new MockChannel("123456"); const msg = processor.StateEventToMessage(event, channel as any); @@ -224,13 +230,13 @@ describe("MatrixEventProcessor", () => { it("Should echo leaves", () => { const processor = createMatrixEventProcessor(); const event = { - sender: "@user:localhost", - type: "m.room.member", content: { membership: "leave", }, - unsigned: {}, + sender: "@user:localhost", state_key: "@user:localhost", + type: "m.room.member", + unsigned: {}, }; const channel = new MockChannel("123456"); const msg = processor.StateEventToMessage(event, channel as any); @@ -239,13 +245,13 @@ describe("MatrixEventProcessor", () => { it("Should echo bans", () => { const processor = createMatrixEventProcessor(); const event = { - sender: "@user:localhost", - type: "m.room.member", content: { membership: "ban", }, - unsigned: {}, + sender: "@user:localhost", state_key: "@user2:localhost", + type: "m.room.member", + unsigned: {}, }; const channel = new MockChannel("123456"); const msg = processor.StateEventToMessage(event, channel as any); @@ -256,13 +262,14 @@ describe("MatrixEventProcessor", () => { it("Should contain a profile.", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "testcontent", }, - }, { - displayname: "Test User", + sender: "@test:localhost", + }, + { avatar_url: "mxc://localhost/avatarurl", + displayname: "Test User", }, mockChannel as any); const author = embeds.messageEmbed.author; Chai.assert.equal(author.name, "Test User"); @@ -273,10 +280,10 @@ describe("MatrixEventProcessor", () => { it("Should contain the users displayname if it exists.", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "testcontent", }, + sender: "@test:localhost", }, { displayname: "Test User"}, mockChannel as any); const author = embeds.messageEmbed.author; @@ -288,10 +295,10 @@ describe("MatrixEventProcessor", () => { it("Should contain the users userid if the displayname is not set", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "testcontent", }, + sender: "@test:localhost", }, null, mockChannel as any); const author = embeds.messageEmbed.author; Chai.assert.equal(author.name, "@test:localhost"); @@ -302,10 +309,10 @@ describe("MatrixEventProcessor", () => { it("Should use the userid when the displayname is too short", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "testcontent", }, + sender: "@test:localhost", }, { displayname: "t"}, mockChannel as any); const author = embeds.messageEmbed.author; @@ -315,10 +322,10 @@ describe("MatrixEventProcessor", () => { it("Should use the userid when displayname is too long", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "testcontent", }, + sender: "@test:localhost", }, { displayname: "this is a very very long displayname that should be capped", }, mockChannel as any); @@ -329,10 +336,10 @@ describe("MatrixEventProcessor", () => { it("Should cap the sender name if it is too long", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@testwithalottosayaboutitselfthatwillgoonandonandonandon:localhost", content: { body: "testcontent", }, + sender: "@testwithalottosayaboutitselfthatwillgoonandonandonandon:localhost", }, null, mockChannel as any); const author = embeds.messageEmbed.author; Chai.assert.equal(author.name, "@testwithalottosayaboutitselftha"); @@ -341,10 +348,10 @@ describe("MatrixEventProcessor", () => { it("Should contain the users avatar if it exists.", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "testcontent", }, + sender: "@test:localhost", }, {avatar_url: "mxc://localhost/test"}, mockChannel as any); const author = embeds.messageEmbed.author; Chai.assert.equal(author.name, "@test:localhost"); @@ -355,10 +362,10 @@ describe("MatrixEventProcessor", () => { it("Should enable mentions if configured.", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "@testuser2 Hello!", }, + sender: "@test:localhost", }, {avatar_url: "test"}, mockChannel as any); Chai.assert.equal(embeds.messageEmbed.description, "<@!12345> Hello!"); }); @@ -366,10 +373,10 @@ describe("MatrixEventProcessor", () => { it("Should disable mentions if configured.", async () => { const processor = createMatrixEventProcessor(true); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "@testuser2 Hello!", }, + sender: "@test:localhost", }, {avatar_url: "test"}, mockChannel as any); Chai.assert.equal(embeds.messageEmbed.description, "@testuser2 Hello!"); }); @@ -377,10 +384,10 @@ describe("MatrixEventProcessor", () => { it("Should remove everyone mentions if configured.", async () => { const processor = createMatrixEventProcessor(false, true); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "@everyone Hello!", }, + sender: "@test:localhost", }, {avatar_url: "test"}, mockChannel as any); Chai.assert.equal(embeds.messageEmbed.description, "@ everyone Hello!"); }); @@ -388,10 +395,10 @@ describe("MatrixEventProcessor", () => { it("Should remove here mentions if configured.", async () => { const processor = createMatrixEventProcessor(false, false, true); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "@here Hello!", }, + sender: "@test:localhost", }, {avatar_url: "test"}, mockChannel as any); Chai.assert.equal(embeds.messageEmbed.description, "@ here Hello!"); }); @@ -406,10 +413,10 @@ describe("MatrixEventProcessor", () => { emojis: mockCollectionEmojis, }); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "I like :supercake:", }, + sender: "@test:localhost", }, {avatar_url: "test"}, mockChannelEmojis as any); Chai.assert.equal( embeds.messageEmbed.description, @@ -427,10 +434,10 @@ describe("MatrixEventProcessor", () => { emojis: mockCollectionEmojis, }); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "I like :lamecake:", }, + sender: "@test:localhost", }, {avatar_url: "test"}, mockChannelEmojis as any); Chai.assert.equal( embeds.messageEmbed.description, @@ -440,11 +447,11 @@ describe("MatrixEventProcessor", () => { it("Should replace /me with * displayname, and italicize message", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", content: { body: "likes puppies", msgtype: "m.emote", }, + sender: "@test:localhost", }, { displayname: "displayname", }, mockChannel as any); @@ -456,12 +463,12 @@ describe("MatrixEventProcessor", () => { it("Should handle stickers.", async () => { const processor = createMatrixEventProcessor(); const embeds = await processor.EventToEmbed({ - sender: "@test:localhost", - type: "m.sticker", content: { body: "Bunnies", url: "mxc://bunny", }, + sender: "@test:localhost", + type: "m.sticker", }, {avatar_url: "test"}, mockChannel as any); Chai.assert.equal(embeds.messageEmbed.description, ""); }); @@ -472,9 +479,9 @@ describe("MatrixEventProcessor", () => { const guild: any = new MockGuild("123", []); const members: Discord.GuildMember[] = [new Discord.GuildMember(guild, { user: { - username: "TestUsername", - id: "12345", discriminator: "54321", + id: "12345", + username: "TestUsername", }, })]; Chai.assert.equal( @@ -500,20 +507,20 @@ describe("MatrixEventProcessor", () => { const members: Discord.GuildMember[] = [new Discord.GuildMember(guild, { nick: "Test", user: { - username: "Test", id: "54321", + username: "Test", }, }), new Discord.GuildMember(guild, { nick: "TestNickname", user: { - username: "TestUsername", id: "12345", + username: "TestUsername", }, }), new Discord.GuildMember(guild, { nick: "𝖘𝖔𝖒𝖊𝖋𝖆𝖓𝖈𝖞𝖓𝖎𝖈𝖐𝖓𝖆𝖒𝖊", user: { - username: "SomeFancyNickname", id: "66666", + username: "SomeFancyNickname", }, })]; Chai.assert.equal(processor.FindMentionsInPlainBody("Hello TestNickname", members), "Hello <@!12345>"); @@ -556,15 +563,15 @@ describe("MatrixEventProcessor", () => { const members: Discord.GuildMember[] = [new Discord.GuildMember(guild, { nick: "that", user: { - username: "TestUsername", id: "12345", + username: "TestUsername", }, }), new Discord.GuildMember(guild, { nick: "testingstring", user: { - username: "that", id: "12345", + username: "that", }, })]; const msg = "Welcome thatman"; @@ -586,8 +593,8 @@ describe("MatrixEventProcessor", () => { const processor = createMatrixEventProcessor(); return expect(processor.HandleAttachment({ content: { - msgtype: "m.video", body: "filename.webm", + msgtype: "m.video", url: "mxc://localhost/200", }, }, mxClient)).to.eventually.satisfy((attachment) => { @@ -600,10 +607,10 @@ describe("MatrixEventProcessor", () => { const processor = createMatrixEventProcessor(); return expect(processor.HandleAttachment({ content: { - msgtype: "m.video", info: { size: 1, }, + msgtype: "m.video", }, }, mxClient)).to.eventually.eq(""); }); @@ -612,11 +619,11 @@ describe("MatrixEventProcessor", () => { const processor = createMatrixEventProcessor(); return expect(processor.HandleAttachment({ content: { - msgtype: "m.video", + body: "filename.webm", info: { size: LARGE_FILE, }, - body: "filename.webm", + msgtype: "m.video", url: "mxc://localhost/8000000", }, }, mxClient)).to.eventually.eq("[filename.webm](https://localhost/8000000)"); @@ -625,11 +632,11 @@ describe("MatrixEventProcessor", () => { const processor = createMatrixEventProcessor(); return expect(processor.HandleAttachment({ content: { - msgtype: "m.video", + body: "filename.webm", info: { size: SMALL_FILE, }, - body: "filename.webm", + msgtype: "m.video", url: "mxc://localhost/200", }, }, mxClient)).to.eventually.satisfy((attachment) => { @@ -642,11 +649,11 @@ describe("MatrixEventProcessor", () => { const processor = createMatrixEventProcessor(); return expect(processor.HandleAttachment({ content: { - msgtype: "m.video", + body: "filename.webm", info: { size: 200, }, - body: "filename.webm", + msgtype: "m.video", url: "mxc://localhost/8000000", }, }, mxClient)).to.eventually.eq("[filename.webm](https://localhost/8000000)"); @@ -654,15 +661,15 @@ describe("MatrixEventProcessor", () => { it("Should handle stickers.", () => { const processor = createMatrixEventProcessor(); return expect(processor.HandleAttachment({ - sender: "@test:localhost", - type: "m.sticker", content: { body: "Bunnies", - url: "mxc://bunny", info: { mimetype: "image/png", }, + url: "mxc://bunny", }, + sender: "@test:localhost", + type: "m.sticker", }, mxClient)).to.eventually.satisfy((attachment) => { expect(attachment.name).to.eq("Bunnies.png"); return true; @@ -673,19 +680,17 @@ describe("MatrixEventProcessor", () => { it("should handle reply-less events", async () => { const processor = createMatrixEventProcessor(); const result = await processor.GetEmbedForReply({ - sender: "@test:localhost", - type: "m.room.message", content: { body: "Test", }, + sender: "@test:localhost", + type: "m.room.message", }); expect(result).to.be.undefined; }); it("should handle replies without a fallback", async () => { const processor = createMatrixEventProcessor(); const result = await processor.GetEmbedForReply({ - sender: "@test:localhost", - type: "m.room.message", content: { "body": "Test", "m.relates_to": { @@ -694,6 +699,8 @@ describe("MatrixEventProcessor", () => { }, }, }, + sender: "@test:localhost", + type: "m.room.message", }); expect(result[0].description).to.be.equal("Hello!"); expect(result[0].author.name).to.be.equal("Doggo!"); @@ -704,8 +711,6 @@ describe("MatrixEventProcessor", () => { it("should handle replies with a missing event", async () => { const processor = createMatrixEventProcessor(); const result = await processor.GetEmbedForReply({ - sender: "@test:localhost", - type: "m.room.message", content: { "body": `> <@doggo:localhost> This is the fake body @@ -716,6 +721,8 @@ This is where the reply goes`, }, }, }, + sender: "@test:localhost", + type: "m.room.message", }); expect(result[0].description).to.be.equal("Reply with unknown content"); expect(result[0].author.name).to.be.equal("Unknown"); @@ -726,8 +733,6 @@ This is where the reply goes`, it("should handle replies with a valid reply event", async () => { const processor = createMatrixEventProcessor(); const result = await processor.GetEmbedForReply({ - sender: "@test:localhost", - type: "m.room.message", content: { "body": `> <@doggo:localhost> This is the original body @@ -738,6 +743,8 @@ This is where the reply goes`, }, }, }, + sender: "@test:localhost", + type: "m.room.message", }); expect(result[0].description).to.be.equal("Hello!"); expect(result[0].author.name).to.be.equal("Doggo!"); @@ -748,8 +755,6 @@ This is where the reply goes`, it("should handle replies on top of replies", async () => { const processor = createMatrixEventProcessor(); const result = await processor.GetEmbedForReply({ - sender: "@test:localhost", - type: "m.room.message", content: { "body": `> <@doggo:localhost> This is the first reply @@ -760,6 +765,8 @@ This is the second reply`, }, }, }, + sender: "@test:localhost", + type: "m.room.message", }); expect(result[0].description).to.be.equal("This is the first reply"); expect(result[0].author.name).to.be.equal("Doggo!"); @@ -770,8 +777,6 @@ This is the second reply`, it("should handle replies with non text events", async () => { const processor = createMatrixEventProcessor(); const result = await processor.GetEmbedForReply({ - sender: "@test:localhost", - type: "m.room.message", content: { "body": `> <@doggo:localhost> sent an image. @@ -782,6 +787,8 @@ This is the reply`, }, }, }, + sender: "@test:localhost", + type: "m.room.message", }); expect(result[0].description).to.be.equal("Reply with unknown content"); expect(result[0].author.name).to.be.equal("Doggo!"); diff --git a/test/test_matrixroomhandler.ts b/test/test_matrixroomhandler.ts index 8d2e7f6281b898a65b886912e9bfb185ae03823b..4b4004dd2d971a314048bae196f0bba1c23a0da7 100644 --- a/test/test_matrixroomhandler.ts +++ b/test/test_matrixroomhandler.ts @@ -13,6 +13,9 @@ import {MockGuild} from "./mocks/guild"; import {Guild} from "discord.js"; import { Util } from "../src/util"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -24,11 +27,11 @@ const RoomHandler = (Proxyquire("../src/matrixroomhandler", { "./util": { Util: { DelayedPromise: Util.DelayedPromise, - MsgToArgs: Util.MsgToArgs, - ParseCommand: Util.ParseCommand, GetMxidFromName: () => { return "@123456:localhost"; }, + MsgToArgs: Util.MsgToArgs, + ParseCommand: Util.ParseCommand, }, }, })).MatrixRoomHandler; @@ -53,24 +56,24 @@ function createRH(opts: any = {}) { USERSBANNED = 0; USERSUNBANNED = 0; const bridge = { + getBot: () => { + return { + isRemoteUser: (id) => { + return id !== undefined && id.startsWith("@_discord_"); + }, + }; + }, getIntent: () => { return { - sendMessage: (roomId, content) => Promise.resolve(content), + ban: () => { USERSBANNED++; return Promise.resolve(); }, getClient: () => mxClient, join: () => { USERSJOINED++; }, - leave: () => { }, kick: () => { USERSKICKED++; return Promise.resolve(); }, - ban: () => { USERSBANNED++; return Promise.resolve(); }, + leave: () => { }, + sendMessage: (roomId, content) => Promise.resolve(content), unban: () => { USERSUNBANNED++; return Promise.resolve(); }, }; }, - getBot: () => { - return { - isRemoteUser: (id) => { - return id !== undefined && id.startsWith("@_discord_"); - }, - }; - }, getRoomStore: () => { return { removeEntriesByMatrixRoomId: () => { @@ -80,17 +83,19 @@ function createRH(opts: any = {}) { }, }; const us = { + EnsureJoin: () => Promise.resolve(), OnMemberState: () => Promise.resolve("user_sync_handled"), OnUpdateUser: () => Promise.resolve(), - EnsureJoin: () => Promise.resolve(), }; const cs = { - OnUpdate: () => Promise.resolve(), GetRoomIdsFromChannel: (chan) => { return Promise.resolve(["#" + chan.id + ":localhost"]); }, + OnUpdate: () => Promise.resolve(), }; const bot = { + ChannelSyncroniser: cs, + GetBotId: () => "bot12345", GetChannelFromRoomId: (roomid: string) => { if (roomid === "!accept:localhost") { const guild = new MockGuild("666666"); @@ -106,10 +111,10 @@ function createRH(opts: any = {}) { return Promise.reject("Roomid not found"); } }, - GetBotId: () => "bot12345", - ProcessMatrixRedact: () => Promise.resolve("redacted"), - ProcessMatrixMsgEvent: () => Promise.resolve("processed"), - ProcessMatrixStateEvent: () => Promise.resolve("stateevent"), + GetGuilds: () => [new MockGuild("123", [])], + GetIntentFromDiscordMember: () => { + return bridge.getIntent(); + }, LookupRoom: (guildid, discordid) => { if (guildid !== "123") { return Promise.reject("Guild not found"); @@ -119,15 +124,13 @@ function createRH(opts: any = {}) { const channel = new MockChannel(); return Promise.resolve({channel, botUser: true }); }, - GetGuilds: () => [new MockGuild("123", [])], + ProcessMatrixMsgEvent: () => Promise.resolve("processed"), + ProcessMatrixRedact: () => Promise.resolve("redacted"), + ProcessMatrixStateEvent: () => Promise.resolve("stateevent"), ThirdpartySearchForChannels: () => { return []; }, - GetIntentFromDiscordMember: () => { - return bridge.getIntent(); - }, UserSyncroniser: us, - ChannelSyncroniser: cs, }; const config = new DiscordBridgeConfig(); config.limits.roomGhostJoinDelay = 0; @@ -137,19 +140,19 @@ function createRH(opts: any = {}) { config.bridge.enableSelfServiceBridging = true; } const mxClient = { - joinRoom: () => { - USERSJOINED++; - return Promise.resolve(); - }, getStateEvent: () => { return Promise.resolve(opts.powerLevels || {}); }, - setRoomDirectoryVisibilityAppService: () => { + joinRoom: () => { + USERSJOINED++; return Promise.resolve(); }, sendReadReceipt: () => { return Promise.resolve(); }, + setRoomDirectoryVisibilityAppService: () => { + return Promise.resolve(); + }, }; const provisioner = { AskBridgePermission: () => { @@ -257,7 +260,9 @@ describe("MatrixRoomHandler", () => { }, }; return expect(handler.OnEvent(buildRequest({ - type: "m.room.message", content: {body: "abc"}}), context)).to.eventually.equal("processed"); + content: {body: "abc"}, + type: "m.room.message", + }), context)).to.eventually.equal("processed"); }); it("should alert if encryption is turned on", () => { const handler = createRH(); @@ -269,14 +274,17 @@ describe("MatrixRoomHandler", () => { }, }; return expect(handler.OnEvent(buildRequest({ - type: "m.room.encryption", room_id: "!accept:localhost"}), context)).to.eventually.be.fulfilled; + room_id: "!accept:localhost", + type: "m.room.encryption", + }), context)).to.eventually.be.fulfilled; }); it("should process !discord commands", () => { const handler = createRH(); handler.ProcessCommand = (ev) => Promise.resolve("processedcmd"); return expect(handler.OnEvent(buildRequest({ - type: "m.room.message", content: {body: "!discord cmd"}}), null)) - .to.eventually.equal("processedcmd"); + content: {body: "!discord cmd"}, + type: "m.room.message", + }), null)).to.eventually.equal("processedcmd"); }); it("should ignore regular messages with no linked room", () => { const handler = createRH(); @@ -286,8 +294,9 @@ describe("MatrixRoomHandler", () => { }, }; return expect(handler.OnEvent(buildRequest({ - type: "m.room.message", content: {body: "abc"}}), context)) - .to.be.rejectedWith("Event not processed by bridge"); + content: {body: "abc"}, + type: "m.room.message", + }), context)).to.be.rejectedWith("Event not processed by bridge"); }); it("should process stickers", () => { const handler = createRH(); @@ -299,11 +308,11 @@ describe("MatrixRoomHandler", () => { }, }; return expect(handler.OnEvent(buildRequest({ - type: "m.sticker", content: { body: "abc", url: "mxc://abc", }, + type: "m.sticker", }), context)).to.eventually.equal("processed"); }); }); @@ -329,8 +338,8 @@ describe("MatrixRoomHandler", () => { return expect(handler.ProcessCommand({ room_id: "!123:localhost", })).to.eventually.be.deep.equal({ - msgtype: "m.notice", body: "The owner of this bridge does not permit self-service bridging.", + msgtype: "m.notice", }); }); it("should warn if user is not powerful enough with defaults", () => { @@ -338,8 +347,8 @@ describe("MatrixRoomHandler", () => { return expect(handler.ProcessCommand({ room_id: "!123:localhost", })).to.eventually.be.deep.equal({ - msgtype: "m.notice", body: "You do not have the required power level in this room to create a bridge to a Discord channel.", + msgtype: "m.notice", }); }); it("should warn if user is not powerful enough with custom state default", () => { @@ -349,8 +358,8 @@ describe("MatrixRoomHandler", () => { return expect(handler.ProcessCommand({ room_id: "!123:localhost", })).to.eventually.be.deep.equal({ - msgtype: "m.notice", body: "You do not have the required power level in this room to create a bridge to a Discord channel.", + msgtype: "m.notice", }); }); it("should allow if user is powerful enough with defaults", () => { @@ -358,8 +367,8 @@ describe("MatrixRoomHandler", () => { users_default: 60, }}); return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord help"}, + room_id: "!123:localhost", }).then((evt) => { return expect(evt.body.startsWith("Available commands")).to.be.true; }); @@ -371,9 +380,9 @@ describe("MatrixRoomHandler", () => { }, }}); return handler.ProcessCommand({ + content: {body: "!discord help"}, room_id: "!123:localhost", sender: "@user:localhost", - content: {body: "!discord help"}, }).then((evt) => { return expect(evt.body.startsWith("Available commands")).to.be.true; }); @@ -385,57 +394,67 @@ describe("MatrixRoomHandler", () => { }}); const context = {rooms: {}}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord bridge 123 456"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.be.eq("I have bridged this room to your channel"); }); }); it("will fail to bridge if permissions were denied", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + denyBridgePermission: true, + powerLevels: { users_default: 100, - }, denyBridgePermission: true}); + }, + }); const context = {rooms: {}}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord bridge 123 456"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.be.eq("The bridge has been declined by the Discord guild"); }); }); it("will fail to bridge if permissions were denied", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + failBridgeMatrix: true, + powerLevels: { users_default: 100, - }, failBridgeMatrix: true}); + }, + }); const context = {rooms: {}}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord bridge 123 456"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.be .eq("There was a problem bridging that channel - has the guild owner approved the bridge?"); }); }); it("will not bridge if a link already exists", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + powerLevels: { users_default: 100, - }}); + }, + }); const context = {rooms: { remote: true }}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord bridge"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.be.eq("This room is already bridged to a Discord guild."); }); }); it("will not bridge without required args", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + powerLevels: { users_default: 100, - }}); + }, + }); const context = {rooms: {}}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord bridge"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.contain("Invalid syntax"); }); @@ -443,59 +462,70 @@ describe("MatrixRoomHandler", () => { }); describe("!discord unbridge", () => { it("will unbridge", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + powerLevels: { users_default: 100, - }}); + }, + }); const context = {rooms: { remote: { data: { plumbed: true, }, - } }}; + } }}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord unbridge"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.be.eq("This room has been unbridged"); }); }); it("will not unbridge if a link does not exist", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + powerLevels: { users_default: 100, - }}); + }, + }); const context = {rooms: { remote: undefined }}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord unbridge"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.be.eq("This room is not bridged."); }); }); it("will not unbridge non-plumbed rooms", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + powerLevels: { users_default: 100, - }}); + }, + }); const context = {rooms: { remote: { - data: { - plumbed: false, - }}}}; + data: { + plumbed: false, + }, + }}}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord unbridge"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.be.eq("This room cannot be unbridged."); }); }); it("will show error if unbridge fails", () => { - const handler: any = createRH({powerLevels: { + const handler: any = createRH({ + failUnbridge: true, + powerLevels: { users_default: 100, - }, failUnbridge: true}); + }, + }); const context = {rooms: { remote: { - data: { - plumbed: true, - }}}}; + data: { + plumbed: true, + }, + }}}; return handler.ProcessCommand({ - room_id: "!123:localhost", content: {body: "!discord unbridge"}, + room_id: "!123:localhost", }, context).then((evt) => { return expect(evt.body).to.contain("There was an error unbridging this room."); }); @@ -548,8 +578,8 @@ describe("MatrixRoomHandler", () => { it("will return an array", () => { const handler: any = createRH({}); return handler.tpGetLocation("", { - guild_id: "", channel_name: "", + guild_id: "", }).then((channels) => { expect(channels).to.be.a("array"); }); @@ -597,6 +627,7 @@ describe("MatrixRoomHandler", () => { const intent = { getClient: () => { return { + getUserId: () => "@test:localhost", joinRoom: () => { if (shouldFail) { shouldFail = false; @@ -604,7 +635,6 @@ describe("MatrixRoomHandler", () => { } return Promise.resolve(); }, - getUserId: () => "@test:localhost", }; }, }; @@ -637,8 +667,8 @@ describe("MatrixRoomHandler", () => { }; const message = { channel, - member, content: "!matrix kick someuser", + member, }; return handler.HandleDiscordCommand(message).then(() => { expect(USERSKICKED).equals(1); @@ -655,8 +685,8 @@ describe("MatrixRoomHandler", () => { }; const message = { channel, - member, content: "!matrix kick someuser", + member, }; return handler.HandleDiscordCommand(message).then(() => { // tslint:disable-next-line:no-magic-numbers @@ -674,8 +704,8 @@ describe("MatrixRoomHandler", () => { }; const message = { channel, - member, content: "!matrix kick someuser", + member, }; return handler.HandleDiscordCommand(message).then(() => { expect(USERSKICKED).equals(0); @@ -692,8 +722,8 @@ describe("MatrixRoomHandler", () => { }; const message = { channel, - member, content: "!matrix ban someuser", + member, }; return handler.HandleDiscordCommand(message).then(() => { expect(USERSBANNED).equals(1); @@ -710,8 +740,8 @@ describe("MatrixRoomHandler", () => { }; const message = { channel, - member, content: "!matrix unban someuser", + member, }; return handler.HandleDiscordCommand(message).then(() => { expect(USERSUNBANNED).equals(1); diff --git a/test/test_messageprocessor.ts b/test/test_messageprocessor.ts index ccdb5bbfe14eb6366db53140c01b78563db5cb57..d9a9d2b805dcd6d20b6c549a429c7141953f2e04 100644 --- a/test/test_messageprocessor.ts +++ b/test/test_messageprocessor.ts @@ -6,6 +6,9 @@ import { DiscordBot } from "../src/bot"; import { MockGuild } from "./mocks/guild"; import { MockMember } from "./mocks/member"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const bot = { @@ -79,6 +82,7 @@ describe("MessageProcessor", () => { color: null, createdAt: null, createdTimestamp: null, + description: "Description", fields: null, footer: null, hexColor: null, @@ -86,11 +90,10 @@ describe("MessageProcessor", () => { message: null, provider: null, thumbnail: null, - type: null, - video: null, title: "Title", - description: "Description", + type: null, url: "http://example.com", + video: null, }, ]; msg.content = "message"; @@ -294,8 +297,8 @@ describe("MessageProcessor", () => { const msg = new Discord.Message(null, null, null); msg.embeds = [ new Discord.MessageEmbed(msg, { - title: "TestTitle", description: "TestDescription", + title: "TestTitle", }), ]; const inContent = ""; @@ -307,9 +310,9 @@ describe("MessageProcessor", () => { const msg = new Discord.Message(null, null, null); msg.embeds = [ new Discord.MessageEmbed(msg, { + description: "TestDescription", title: "TestTitle", url: "testurl", - description: "TestDescription", }), ]; const inContent = ""; @@ -333,14 +336,14 @@ describe("MessageProcessor", () => { const msg = new Discord.Message(null, null, null); msg.embeds = [ new Discord.MessageEmbed(msg, { + description: "TestDescription", title: "TestTitle", url: "testurl", - description: "TestDescription", }), new Discord.MessageEmbed(msg, { + description: "TestDescription2", title: "TestTitle2", url: "testurl2", - description: "TestDescription2", }), ]; const inContent = ""; @@ -355,9 +358,9 @@ describe("MessageProcessor", () => { const msg = new Discord.Message(null, null, null); msg.embeds = [ new Discord.MessageEmbed(msg, { + description: "TestDescription", title: "TestTitle", url: "testurl", - description: "TestDescription", }), ]; const inContent = "Content that goes in the message"; diff --git a/test/test_presencehandler.ts b/test/test_presencehandler.ts index c6c652cbcacde39c0d275d73c84d4600eb9f804c..0cb21732795d80fb200a7356796c9c520a37085d 100644 --- a/test/test_presencehandler.ts +++ b/test/test_presencehandler.ts @@ -7,12 +7,18 @@ import { PresenceHandler } from "../src/presencehandler"; import { DiscordBot } from "../src/bot"; import { MockUser } from "./mocks/user"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; const INTERVAL = 250; let lastStatus = null; // const assert = Chai.assert; const bot = { + GetBotId: () => { + return "1234"; + }, GetIntentFromDiscordMember: (member) => { return { getClient: () => { @@ -25,9 +31,6 @@ const bot = { }, }; }, - GetBotId: () => { - return "1234"; - }, }; describe("PresenceHandler", () => { @@ -140,8 +143,8 @@ describe("PresenceHandler", () => { status_msg: "Do not disturb", }); member.MockSetPresence(new Discord.Presence({ - status: "dnd", game: new Discord.Game({name: "Test Game"}), + status: "dnd", })); handler.ProcessUser(member); Chai.assert.deepEqual(lastStatus, { @@ -154,8 +157,8 @@ describe("PresenceHandler", () => { const handler = new PresenceHandler(bot as DiscordBot); const member = new MockUser("abc", "def") as any; member.MockSetPresence(new Discord.Presence({ - status: "online", game: new Discord.Game({name: "Test Game"}), + status: "online", })); handler.ProcessUser(member); Chai.assert.deepEqual(lastStatus, { @@ -163,8 +166,8 @@ describe("PresenceHandler", () => { status_msg: "Playing Test Game", }); member.MockSetPresence(new Discord.Presence({ - status: "online", game: new Discord.Game({name: "Test Game", type: 1}), + status: "online", })); handler.ProcessUser(member); Chai.assert.deepEqual(lastStatus, { diff --git a/test/test_store.ts b/test/test_store.ts index 9b1956a24e73bf0e427e94749ca03c7300f381a8..6dfc9aaf72898357144f3cdec4e32dbd754c5d46 100644 --- a/test/test_store.ts +++ b/test/test_store.ts @@ -5,6 +5,10 @@ import { DiscordStore, CURRENT_SCHEMA } from "../src/store"; import { DbEmoji } from "../src/db/dbdataemoji"; import { DbEvent } from "../src/db/dbdataevent"; import { Log } from "../src/log"; + +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; diff --git a/test/test_usersyncroniser.ts b/test/test_usersyncroniser.ts index c1daddc4c3d0d17e388e022f8a6bee38a7a9f0c5..8e53c14d28dccc76dae769da30f6228db3bc01d7 100644 --- a/test/test_usersyncroniser.ts +++ b/test/test_usersyncroniser.ts @@ -9,6 +9,9 @@ import {MockMember} from "./mocks/member"; import {MockGuild} from "./mocks/guild"; import { MockChannel } from "./mocks/channel"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -49,31 +52,6 @@ function CreateUserSync(remoteUsers: any[] = []): UserSyncroniser { SEV_KEY = null; SEV_COUNT = 0; const bridge: any = { - getUserStore: () => { - REMOTEUSER_SET = null; - LINK_RM_USER = null; - LINK_MX_USER = null; - return { - getRemoteUser: (id) => { - const user = remoteUsers.find((u) => u.id === id); - if (user === undefined) { - return null; - } - return user; - }, - getRemoteUsersFromMatrixId: (id) => { - return remoteUsers.filter((u) => u.id === id); - }, - setRemoteUser: (remoteUser) => { - REMOTEUSER_SET = remoteUser; - return Promise.resolve(); - }, - linkUsers: (mxUser, remoteUser) => { - LINK_MX_USER = mxUser; - LINK_RM_USER = remoteUser; - }, - }; - }, getIntent: (id) => { DISPLAYNAME_SET = null; AVATAR_SET = null; @@ -82,6 +60,16 @@ function CreateUserSync(remoteUsers: any[] = []): UserSyncroniser { JOINS = 0; LEAVES = 0; return { + getClient: () => { + return { + sendStateEvent: (roomId, type, content, key) => { + SEV_ROOM_ID = roomId; + SEV_CONTENT = content; + SEV_KEY = key; + SEV_COUNT++; + }, + }; + }, join: (roomId) => { JOIN_ROOM_ID = roomId; JOINS++; @@ -90,34 +78,43 @@ function CreateUserSync(remoteUsers: any[] = []): UserSyncroniser { LEAVE_ROOM_ID = roomId; LEAVES++; }, + setAvatarUrl: (ava) => { + AVATAR_SET = ava; + return Promise.resolve(); + }, setDisplayName: (dn) => { DISPLAYNAME_SET = dn; return Promise.resolve(); }, - setAvatarUrl: (ava) => { - AVATAR_SET = ava; - return Promise.resolve(); + }; + }, + getUserStore: () => { + REMOTEUSER_SET = null; + LINK_RM_USER = null; + LINK_MX_USER = null; + return { + getRemoteUser: (id) => { + const user = remoteUsers.find((u) => u.id === id); + if (user === undefined) { + return null; + } + return user; }, - getClient: () => { - return { - sendStateEvent: (roomId, type, content, key) => { - SEV_ROOM_ID = roomId; - SEV_CONTENT = content; - SEV_KEY = key; - SEV_COUNT++; - }, - }; + getRemoteUsersFromMatrixId: (id) => { + return remoteUsers.filter((u) => u.id === id); + }, + linkUsers: (mxUser, remoteUser) => { + LINK_MX_USER = mxUser; + LINK_RM_USER = remoteUser; + }, + setRemoteUser: (remoteUser) => { + REMOTEUSER_SET = remoteUser; + return Promise.resolve(); }, }; }, }; const discordbot: any = { - GetRoomIdsFromGuild: () => { - return Promise.resolve(GUILD_ROOM_IDS); - }, - GetIntentFromDiscordMember: (id) => { - return bridge.getIntent(id); - }, GetChannelFromRoomId: (id) => { if (id === "!found:localhost") { const guild = new MockGuild("666666"); @@ -131,6 +128,12 @@ function CreateUserSync(remoteUsers: any[] = []): UserSyncroniser { GetGuilds: () => { return []; }, + GetIntentFromDiscordMember: (id) => { + return bridge.getIntent(id); + }, + GetRoomIdsFromGuild: () => { + return Promise.resolve(GUILD_ROOM_IDS); + }, }; const config = new DiscordBridgeConfig(); config.bridge.domain = "localhost"; @@ -231,12 +234,12 @@ describe("UserSyncroniser", () => { it("Will create a new user", () => { const userSync = CreateUserSync(); const state: IUserState = { - id: "123456", + avatarId: null, + avatarUrl: null, // Nullable createUser: true, - mxUserId: "@_discord_123456:localhost", displayName: null, // Nullable - avatarUrl: null, // Nullable - avatarId: null, + id: "123456", + mxUserId: "@_discord_123456:localhost", removeAvatar: false, }; return userSync.ApplyStateToProfile(state).then(() => { @@ -248,12 +251,12 @@ describe("UserSyncroniser", () => { it("Will set a display name", () => { const userSync = CreateUserSync(); const state: IUserState = { - id: "123456", + avatarId: null, + avatarUrl: null, // Nullable createUser: true, - mxUserId: "@_discord_123456:localhost", displayName: "123456", // Nullable - avatarUrl: null, // Nullable - avatarId: null, + id: "123456", + mxUserId: "@_discord_123456:localhost", removeAvatar: false, }; return userSync.ApplyStateToProfile(state).then(() => { @@ -269,12 +272,12 @@ describe("UserSyncroniser", () => { it("Will set an avatar", () => { const userSync = CreateUserSync(); const state: IUserState = { - id: "123456", + avatarId: null, + avatarUrl: "654321", // Nullable createUser: true, - mxUserId: "@_discord_123456:localhost", displayName: null, // Nullable - avatarUrl: "654321", // Nullable - avatarId: null, + id: "123456", + mxUserId: "@_discord_123456:localhost", removeAvatar: false, }; return userSync.ApplyStateToProfile(state).then(() => { @@ -291,12 +294,12 @@ describe("UserSyncroniser", () => { it("Will remove an avatar", () => { const userSync = CreateUserSync(); const state: IUserState = { - id: "123456", + avatarId: null, + avatarUrl: null, // Nullable createUser: true, - mxUserId: "@_discord_123456:localhost", displayName: null, // Nullable - avatarUrl: null, // Nullable - avatarId: null, + id: "123456", + mxUserId: "@_discord_123456:localhost", removeAvatar: true, }; return userSync.ApplyStateToProfile(state).then(() => { @@ -313,12 +316,12 @@ describe("UserSyncroniser", () => { it("will do nothing if nothing needs to be done", () => { const userSync = CreateUserSync([new RemoteUser("123456")]); const state: IUserState = { - id: "123456", + avatarId: null, + avatarUrl: null, // Nullable createUser: false, - mxUserId: "@_discord_123456:localhost", displayName: null, // Nullable - avatarUrl: null, // Nullable - avatarId: null, + id: "123456", + mxUserId: "@_discord_123456:localhost", removeAvatar: false, }; return userSync.ApplyStateToProfile(state).then(() => { @@ -334,9 +337,9 @@ describe("UserSyncroniser", () => { it("Will apply a new nick", () => { const userSync = CreateUserSync([new RemoteUser("123456")]); const state: IGuildMemberState = { + displayName: "Good Boy", id: "123456", mxUserId: "@_discord_123456:localhost", - displayName: "Good Boy", roles: [], }; return userSync.ApplyStateToRoom(state, "!abc:localhost", "123456").then(() => { @@ -350,9 +353,9 @@ describe("UserSyncroniser", () => { it("Will not apply unchanged nick", () => { const userSync = CreateUserSync([new RemoteUser("123456")]); const state: IGuildMemberState = { + displayName: null, id: "123456", mxUserId: "@_discord_123456:localhost", - displayName: null, roles: [], }; return userSync.ApplyStateToRoom(state, "!abc:localhost", "123456").then(() => { @@ -368,13 +371,13 @@ describe("UserSyncroniser", () => { const TESTROLE_COLOR = 1337; const TESTROLE_POSITION = 42; const state: IGuildMemberState = { + displayName: "Good Boy", id: "123456", mxUserId: "@_discord_123456:localhost", - displayName: "Good Boy", roles: [ { - name: TESTROLE_NAME, color: TESTROLE_COLOR, + name: TESTROLE_NAME, position: TESTROLE_POSITION, }, ], @@ -431,8 +434,8 @@ describe("UserSyncroniser", () => { const TESTROLE_POSITION = 42; member.roles = [ { - name: TESTROLE_NAME, color: TESTROLE_COLOR, + name: TESTROLE_NAME, position: TESTROLE_POSITION, }, ]; @@ -557,47 +560,37 @@ describe("UserSyncroniser", () => { const userSync = CreateUserSync([new RemoteUser("123456")]); return Promise.all([ expect(userSync.OnMemberState({ - origin_server_ts: 10000, - content: { - - }, + content: { }, event_id: "Anicent:localhost", + origin_server_ts: 10000, room_id: "!found:localhost", state_key: "123456", }, DELAY_MS)).to.eventually.equal(UserSyncroniser.ERR_NEWER_EVENT, "State 1 Failed"), expect(userSync.OnMemberState({ - origin_server_ts: 7000, - content: { - - }, + content: { }, event_id: "QuiteOld:localhost", + origin_server_ts: 7000, room_id: "!found:localhost", state_key: "123456", }, DELAY_MS)).to.eventually.equal(UserSyncroniser.ERR_NEWER_EVENT, "State 2 Failed"), expect(userSync.OnMemberState({ - origin_server_ts: 3000, - content: { - - }, + content: { }, event_id: "FreshEnough:localhost", + origin_server_ts: 3000, room_id: "!found:localhost", state_key: "123456", }, DELAY_MS)).to.eventually.equal(UserSyncroniser.ERR_NEWER_EVENT, "State 3 Failed"), expect(userSync.OnMemberState({ - origin_server_ts: 4000, - content: { - - }, + content: { }, event_id: "GettingOnABit:localhost", + origin_server_ts: 4000, room_id: "!found:localhost", state_key: "123456", }, DELAY_MS)).to.eventually.equal(UserSyncroniser.ERR_NEWER_EVENT, "State 4 Failed"), expect(userSync.OnMemberState({ - origin_server_ts: 100, - content: { - - }, + content: { }, event_id: "FreshOutTheOven:localhost", + origin_server_ts: 100, room_id: "!found:localhost", state_key: "123456", }, DELAY_MS)).to.eventually.be.fulfilled, diff --git a/test/test_util.ts b/test/test_util.ts index dc48346bf48a8c85bbeaf673c49eb804f24590ec..44ec959cbeb94feb237a67ed92a78a08d1c41158 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -3,6 +3,9 @@ import * as ChaiAsPromised from "chai-as-promised"; import { Util, ICommandAction, ICommandParameters } from "../src/util"; +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -15,11 +18,11 @@ function CreateMockIntent(members) { const ret = []; for (const member of members[url]) { ret.push({ - membership: member.membership, - state_key: member.mxid, content: { displayname: member.displayname, }, + membership: member.membership, + state_key: member.mxid, }); } return { @@ -73,9 +76,9 @@ describe("Util", () => { const mockRooms = { "/rooms/abc/members": [ { + displayname: "GoodBoy", membership: "join", mxid: "@123:localhost", - displayname: "GoodBoy", }, ], }; @@ -88,14 +91,14 @@ describe("Util", () => { const mockRooms = { "/rooms/abc/members": [ { + displayname: "GoodBoy", membership: "join", mxid: "@123:localhost", - displayname: "GoodBoy", }, { + displayname: "GoodBoy", membership: "join", mxid: "@456:localhost", - displayname: "GoodBoy", }, ], }; @@ -106,9 +109,9 @@ describe("Util", () => { const mockRooms = { "/rooms/abc/members": [ { + displayname: "GoodBoy", membership: "join", mxid: "@123:localhost", - displayname: "GoodBoy", }, ], }; diff --git a/tools/addRoomsToDirectory.ts b/tools/addRoomsToDirectory.ts index 9d398a9df72349cc959db5e759170fca2f97475e..0bf11300b261496d882e3ed153f4451f8fd2a92f 100644 --- a/tools/addRoomsToDirectory.ts +++ b/tools/addRoomsToDirectory.ts @@ -13,25 +13,25 @@ import { Log } from "../src/log"; const log = new Log("AddRoomsToDirectory"); const optionDefinitions = [ { - name: "help", alias: "h", - type: Boolean, description: "Display this usage guide.", + name: "help", + type: Boolean, }, { - name: "config", - alias: "c", - type: String, - defaultValue: "config.yaml", - description: "The AS config file.", - typeLabel: "<config.yaml>", + alias: "c", + defaultValue: "config.yaml", + description: "The AS config file.", + name: "config", + type: String, + typeLabel: "<config.yaml>", }, { - name: "store", alias: "s", - type: String, defaultValue: "room-store.db", description: "The location of the room store.", + name: "store", + type: String, }, ]; @@ -41,8 +41,9 @@ if (options.help) { /* tslint:disable:no-console */ console.log(usage([ { + content: "A tool to set all the bridged rooms to visible in the directory.", header: "Add rooms to directory", - content: "A tool to set all the bridged rooms to visible in the directory."}, + }, { header: "Options", optionList: optionDefinitions, @@ -65,12 +66,12 @@ const clientFactory = new ClientFactory({ }); const bridge = new Bridge({ - homeserverUrl: true, - registration: true, - domain: "rubbish", controller: { onEvent: () => { }, }, + domain: "rubbish", + homeserverUrl: true, + registration: true, roomStore: options.store, }); diff --git a/tools/adminme.ts b/tools/adminme.ts index 0f818ae59d0475f0a2bd965b2b52d6f409e0f8b5..2d8cf88a38c659c2980f962d1ebf765fda01484c 100644 --- a/tools/adminme.ts +++ b/tools/adminme.ts @@ -11,61 +11,67 @@ import * as usage from "command-line-usage"; import { DiscordBridgeConfig } from "../src/config"; const optionDefinitions = [ - { - name: "help", - alias: "h", - type: Boolean, - description: "Display this usage guide."}, - { - name: "config", - alias: "c", - type: String, - defaultValue: "config.yaml", - description: "The AS config file.", - typeLabel: "<config.yaml>" }, - { - name: "roomid", - alias: "r", - type: String, - description: "The roomid to modify"}, - { - name: "userid", - alias: "u", - type: String, - description: "The userid to give powers"}, - { - name: "power", - alias: "p", - type: Number, - defaultValue: 100, - description: "The power to set", - typeLabel: "<0-100>" }, + { + alias: "h", + description: "Display this usage guide.", + name: "help", + type: Boolean, + }, + { + alias: "c", + defaultValue: "config.yaml", + description: "The AS config file.", + name: "config", + type: String, + typeLabel: "<config.yaml>", + }, + { + alias: "r", + description: "The roomid to modify", + name: "roomid", + type: String, + }, + { + alias: "u", + description: "The userid to give powers", + name: "userid", + type: String, + }, + { + alias: "p", + defaultValue: 100, + description: "The power to set", + name: "power", + type: Number, + typeLabel: "<0-100>", + }, ]; const options = args(optionDefinitions); if (options.help) { - /* tslint:disable:no-console */ - console.log(usage([ - { - header: "Admin Me", - content: "A tool to give a user a power level in a bot user controlled room."}, - { - header: "Options", - optionList: optionDefinitions, - }, - ])); - process.exit(0); + /* tslint:disable:no-console */ + console.log(usage([ + { + content: "A tool to give a user a power level in a bot user controlled room.", + header: "Admin Me", + }, + { + header: "Options", + optionList: optionDefinitions, + }, + ])); + process.exit(0); } if (!options.roomid) { - console.error("Missing roomid parameter. Check -h"); - process.exit(1); + console.error("Missing roomid parameter. Check -h"); + process.exit(1); } if (!options.userid) { - console.error("Missing userid parameter. Check -h"); - process.exit(1); + console.error("Missing userid parameter. Check -h"); + process.exit(1); } const yamlConfig = yaml.safeLoad(fs.readFileSync("discord-registration.yaml", "utf8")); @@ -73,13 +79,13 @@ const registration = AppServiceRegistration.fromObject(yamlConfig); const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig; if (registration === null) { - throw new Error("Failed to parse registration file"); + throw new Error("Failed to parse registration file"); } const clientFactory = new ClientFactory({ - appServiceUserId: "@" + registration.sender_localpart + ":" + config.bridge.domain, - token: registration.as_token, - url: config.bridge.homeserverUrl, + appServiceUserId: "@" + registration.sender_localpart + ":" + config.bridge.domain, + token: registration.as_token, + url: config.bridge.homeserverUrl, }); const client = clientFactory.getClientAs(); const intent = new Intent(client, client, {registered: true}); diff --git a/tools/chanfix.ts b/tools/chanfix.ts index 3824c28e3225fa293ebe07412f7736f9fe0cd707..e9a1d7d07a69ba908305c8d78eb5844767e44720 100644 --- a/tools/chanfix.ts +++ b/tools/chanfix.ts @@ -16,18 +16,18 @@ const log = new Log("ChanFix"); const optionDefinitions = [ { - name: "help", alias: "h", - type: Boolean, description: "Display this usage guide.", + name: "help", + type: Boolean, }, { - name: "config", - alias: "c", - type: String, - defaultValue: "config.yaml", - description: "The AS config file.", - typeLabel: "<config.yaml>", + alias: "c", + defaultValue: "config.yaml", + description: "The AS config file.", + name: "config", + type: String, + typeLabel: "<config.yaml>", }, ]; @@ -37,9 +37,10 @@ if (options.help) { /* tslint:disable:no-console */ console.log(usage([ { - header: "Fix bridged channels", content: "A tool to fix channels of rooms already bridged " + - "to matrix, to make sure their names, icons etc. are correctly."}, + "to matrix, to make sure their names, icons etc. are correctly.", + header: "Fix bridged channels", + }, { header: "Options", optionList: optionDefinitions, @@ -72,16 +73,16 @@ const bridge = new Bridge({ controller: { onEvent: () => { }, }, + domain: config.bridge.domain, + homeserverUrl: config.bridge.homeserverUrl, intentOptions: { clients: { dontJoin: true, // handled manually }, }, - domain: config.bridge.domain, - homeserverUrl: config.bridge.homeserverUrl, registration, - userStore: config.database.userStorePath, roomStore: config.database.roomStorePath, + userStore: config.database.userStorePath, }); provisioner.SetBridge(bridge); diff --git a/tools/ghostfix.ts b/tools/ghostfix.ts index 94dbedc224a85ca6afd4e987e52584113a2dca13..0b6b0ee21ee51c75d32903a5af485f380785821c 100644 --- a/tools/ghostfix.ts +++ b/tools/ghostfix.ts @@ -28,18 +28,18 @@ const JOIN_ROOM_SCHEDULE = [ const optionDefinitions = [ { - name: "help", alias: "h", - type: Boolean, description: "Display this usage guide.", + name: "help", + type: Boolean, }, { - name: "config", - alias: "c", - type: String, - defaultValue: "config.yaml", - description: "The AS config file.", - typeLabel: "<config.yaml>", + alias: "c", + defaultValue: "config.yaml", + description: "The AS config file.", + name: "config", + type: String, + typeLabel: "<config.yaml>", }, ]; @@ -49,9 +49,10 @@ if (options.help) { /* tslint:disable:no-console */ console.log(usage([ { - header: "Fix usernames of joined ghosts", content: "A tool to fix usernames of ghosts already in " + - "matrix rooms, to make sure they represent the correct discord usernames."}, + "matrix rooms, to make sure they represent the correct discord usernames.", + header: "Fix usernames of joined ghosts", + }, { header: "Options", optionList: optionDefinitions, @@ -84,16 +85,16 @@ const bridge = new Bridge({ controller: { onEvent: () => { }, }, + domain: config.bridge.domain, + homeserverUrl: config.bridge.homeserverUrl, intentOptions: { clients: { dontJoin: true, // handled manually }, }, - domain: config.bridge.domain, - homeserverUrl: config.bridge.homeserverUrl, registration, - userStore: config.database.userStorePath, roomStore: config.database.roomStorePath, + userStore: config.database.userStorePath, }); provisioner.SetBridge(bridge); diff --git a/tools/userClientTools.ts b/tools/userClientTools.ts index 94bd2ad9c3f548aa544a675a1e2b1d8c29649002..26029fde3fc509d32c97bafbed84e969575af4ad 100644 --- a/tools/userClientTools.ts +++ b/tools/userClientTools.ts @@ -14,94 +14,99 @@ const log = new Log("UserClientTools"); const PUPPETING_DOC_URL = "https://github.com/Half-Shot/matrix-appservice-discord/blob/develop/docs/puppeting.md"; const optionDefinitions = [ - { - name: "help", - alias: "h", - type: Boolean, - description: "Display this usage guide."}, - { - name: "config", - alias: "c", - type: String, - defaultValue: "config.yaml", - description: "The AS config file.", - typeLabel: "<config.yaml>" }, - { - name: "add", - type: Boolean, - description: "Add the user to the database."}, - { - name: "remove", - type: Boolean, - description: "Remove the user from the database."}, + { + alias: "h", + description: "Display this usage guide.", + name: "help", + type: Boolean, + }, + { + alias: "c", + defaultValue: "config.yaml", + description: "The AS config file.", + name: "config", + type: String, + typeLabel: "<config.yaml>", + }, + { + description: "Add the user to the database.", + name: "add", + type: Boolean, + }, + { + description: "Remove the user from the database.", + name: "remove", + type: Boolean, + }, ]; const options = args(optionDefinitions); if (options.help || (options.add && options.remove) || !(options.add || options.remove)) { - /* tslint:disable:no-console */ - console.log(usage([ - { - header: "User Client Tools", - content: "A tool to give a user a power level in a bot user controlled room."}, - { - header: "Options", - optionList: optionDefinitions, - }, - ])); - process.exit(0); + /* tslint:disable:no-console */ + console.log(usage([ + { + content: "A tool to give a user a power level in a bot user controlled room.", + header: "User Client Tools", + }, + { + header: "Options", + optionList: optionDefinitions, + }, + ])); + process.exit(0); } const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(options.config, "utf8")); const discordstore = new DiscordStore(config.database ? config.database.filename : "discord.db"); discordstore.init().then(() => { - log.info("Loaded database."); - handleUI(); + log.info("Loaded database."); + handleUI(); }).catch((err) => { - log.info("Couldn't load database. Cannot continue."); - log.info("Ensure the bridge is not running while using this command."); - process.exit(1); + log.info("Couldn't load database. Cannot continue."); + log.info("Ensure the bridge is not running while using this command."); + process.exit(1); }); function handleUI() { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - let userid = null; - let token = null; + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + let userid = null; + let token = null; - rl.question("Please enter your UserID ( ex @Half-Shot:half-shot.uk, @username:matrix.org)", (answeru) => { - userid = answeru; - if (options.add) { - rl.question(` + rl.question("Please enter your UserID ( ex @Half-Shot:half-shot.uk, @username:matrix.org)", (answeru) => { + userid = answeru; + if (options.add) { + rl.question(` Please enter your Discord Token (Instructions for this are on ${PUPPETING_DOC_URL})`, (answert) => { - token = answert; - rl.close(); - addUserToken(userid, token).then(() => { - log.info("Completed successfully"); - process.exit(0); - }).catch((err) => { - log.info("Failed to add, $s", err); - process.exit(1); - }); - }); - } else if (options.remove) { - rl.close(); - discordstore.delete_user_token(userid).then(() => { - log.info("Completed successfully"); - process.exit(0); - }).catch((err) => { - log.info("Failed to delete, $s", err); - process.exit(1); - }); - } - }); + token = answert; + rl.close(); + addUserToken(userid, token).then(() => { + log.info("Completed successfully"); + process.exit(0); + }).catch((err) => { + log.info("Failed to add, $s", err); + process.exit(1); + }); + }); + } else if (options.remove) { + rl.close(); + discordstore.delete_user_token(userid).then(() => { + log.info("Completed successfully"); + process.exit(0); + }).catch((err) => { + log.info("Failed to delete, $s", err); + process.exit(1); + }); + } + }); } function addUserToken(userid: string, token: string): Bluebird<null> { - const clientFactory = new DiscordClientFactory(discordstore); - return clientFactory.getDiscordId(token).then((discordid: string) => { - return discordstore.add_user_token(userid, discordid, token); - }); + const clientFactory = new DiscordClientFactory(discordstore); + return clientFactory.getDiscordId(token).then((discordid: string) => { + return discordstore.add_user_token(userid, discordid, token); + }); }