From 472235034cd3ae926f86e54524c63bf5533d5e35 Mon Sep 17 00:00:00 2001 From: Will Hunt <will@half-shot.uk> Date: Wed, 28 Oct 2020 22:55:16 +0000 Subject: [PATCH] Do the required to make builds pass with better-discord.js --- src/bot.ts | 178 ++++++++++++++++----------- src/channelsyncroniser.ts | 6 +- src/clientfactory.ts | 22 ++-- src/db/schema/dbschema.ts | 1 - src/db/schema/v3.ts | 4 +- src/db/schema/v8.ts | 2 - src/db/schema/v9.ts | 1 - src/discordcommandhandler.ts | 16 ++- src/discordmessageprocessor.ts | 6 +- src/matrixcommandhandler.ts | 2 +- src/matrixeventprocessor.ts | 28 ++--- src/matrixmessageprocessor.ts | 16 +-- src/matrixroomhandler.ts | 2 +- src/presencehandler.ts | 44 ++++--- src/provisioner.ts | 2 +- src/usersyncroniser.ts | 18 +-- src/util.ts | 5 +- test/mocks/channel.ts | 16 ++- test/mocks/collection.ts | 2 +- test/mocks/guild.ts | 10 +- test/mocks/member.ts | 8 +- test/mocks/message.ts | 2 +- test/mocks/user.ts | 2 +- test/test_clientfactory.ts | 2 +- test/test_discordbot.ts | 11 +- test/test_discordmessageprocessor.ts | 19 +-- test/test_matrixeventprocessor.ts | 38 +++--- test/test_presencehandler.ts | 43 ++++--- tools/chanfix.ts | 2 +- tools/ghostfix.ts | 6 +- 30 files changed, 300 insertions(+), 214 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 4468890..65a8968 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -28,7 +28,7 @@ import { UserSyncroniser } from "./usersyncroniser"; import { ChannelSyncroniser } from "./channelsyncroniser"; import { MatrixRoomHandler } from "./matrixroomhandler"; import { Log } from "./log"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import * as mime from "mime"; import { IMatrixEvent, IMatrixMediaInfo, IMatrixMessage } from "./matrixtypes"; import { Appservice, Intent } from "matrix-bot-sdk"; @@ -40,6 +40,7 @@ import { Util } from "./util"; const log = new Log("DiscordBot"); const MIN_PRESENCE_UPDATE_DELAY = 250; +const TYPING_TIMEOUT_MS = 30 * 1000; const CACHE_LIFETIME = 90000; // TODO: This is bad. We should be serving the icon from the own homeserver. @@ -82,6 +83,7 @@ export class DiscordBot { /* Handles messages queued up to be sent to matrix from discord. */ private discordMessageQueue: { [channelId: string]: Promise<void> }; private channelLock: Lock<string>; + private typingTimers: Record<string, NodeJS.Timeout> = {}; // DiscordUser+channel -> Timeout constructor( private config: DiscordBridgeConfig, private bridge: Appservice, @@ -134,10 +136,14 @@ export class DiscordBot { return this.provisioner; } - public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.User, webhookID?: string): Intent { + public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.PartialUser | Discord.User, webhookID: string|null = null): Intent { if (webhookID) { // webhookID and user IDs are the same, they are unique, so no need to prefix _webhook_ - const name = member instanceof Discord.User ? member.username : member.user.username; + const name = member instanceof Discord.GuildMember ? member.user.username : member.username; + if (!name) { + log.error("Couldn't get intent for Discord member, name was null:", member); + throw Error("Couldn't get intent for Discord member, name was null"); + } // TODO: We need to sanitize name return this.bridge.getIntentForSuffix(`${webhookID}_${Util.EscapeStringForUserId(name)}`); } @@ -153,21 +159,16 @@ export class DiscordBot { public async run(): Promise<void> { const client = await this.clientFactory.getClient(); if (!this.config.bridge.disableTypingNotifications) { - client.on("typingStart", async (c, u) => { + client.on("typingStart", async (channel, user) => { try { - await this.OnTyping(c, u, true); + await this.OnTyping(channel, user, true); } catch (err) { log.warning("Exception thrown while handling \"typingStart\" event", err); } }); - client.on("typingStop", async (c, u) => { - try { - await this.OnTyping(c, u, false); - } catch (err) { log.warning("Exception thrown while handling \"typingStop\" event", err); } - }); } if (!this.config.bridge.disablePresence) { - client.on("presenceUpdate", (_, newMember: Discord.GuildMember) => { + client.on("presenceUpdate", (_, newPresence) => { try { - this.presenceHandler.EnqueueUser(newMember.user); + this.presenceHandler.EnqueueUser(newPresence); } catch (err) { log.warning("Exception thrown while handling \"presenceUpdate\" event", err); } }); } @@ -269,21 +270,37 @@ export class DiscordBot { client.on("userUpdate", async (_, user) => { try { + if (!(user instanceof Discord.User)) { + log.warn(`Ignoring update for ${user.username}. User was partial.`); + return; + } await this.userSync.OnUpdateUser(user); } catch (err) { log.error("Exception thrown while handling \"userUpdate\" event", err); } }); - client.on("guildMemberAdd", async (user) => { + client.on("guildMemberAdd", async (member) => { try { - await this.userSync.OnAddGuildMember(user); + if (!(member instanceof Discord.GuildMember)) { + log.warn(`Ignoring update for ${member.guild.id} ${member.id}. User was partial.`); + return; + } + await this.userSync.OnAddGuildMember(member); } catch (err) { log.error("Exception thrown while handling \"guildMemberAdd\" event", err); } }); - client.on("guildMemberRemove", async (user) => { + client.on("guildMemberRemove", async (member) => { try { - await this.userSync.OnRemoveGuildMember(user); + if (!(member instanceof Discord.GuildMember)) { + log.warn(`Ignoring update for ${member.guild.id} ${member.id}. User was partial.`); + return; + } + await this.userSync.OnRemoveGuildMember(member); } catch (err) { log.error("Exception thrown while handling \"guildMemberRemove\" event", err); } }); client.on("guildMemberUpdate", async (_, member) => { try { + if (!(member instanceof Discord.GuildMember)) { + log.warn(`Ignoring update for ${member.guild.id} ${member.id}. User was partial.`); + return; + } await this.userSync.OnUpdateGuildMember(member); } catch (err) { log.error("Exception thrown while handling \"guildMemberUpdate\" event", err); } }); @@ -297,10 +314,10 @@ export class DiscordBot { if (!this.config.bridge.presenceInterval) { this.config.bridge.presenceInterval = MIN_PRESENCE_UPDATE_DELAY; } - this.bot.guilds.forEach((guild) => { - guild.members.forEach((member) => { + this.bot.guilds.cache.forEach((guild) => { + guild.members.cache.forEach((member) => { if (member.id !== this.GetBotId()) { - this.presenceHandler.EnqueueUser(member.user); + this.presenceHandler.EnqueueUser(member.user.presence); } }); }); @@ -311,20 +328,21 @@ export class DiscordBot { } public GetBotId(): string { - return this.bot.user.id; + // TODO: What do we do here? + return this.bot.user!.id; } public GetGuilds(): Discord.Guild[] { - return this.bot.guilds.array(); + return this.bot.guilds.cache.array(); } public ThirdpartySearchForChannels(guildId: string, channelName: string): IThirdPartyLookup[] { 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) => { + if (this.bot.guilds.cache.has(guildId) ) { + const guild = this.bot.guilds.cache.get(guildId); + return guild!.channels.cache.filter((channel) => { return channel.name.toLowerCase() === channelName.toLowerCase(); // Implement searching in the future. }).map((channel) => { return { @@ -347,14 +365,14 @@ export class DiscordBot { const hasSender = sender !== null && sender !== undefined; try { const client = await this.clientFactory.getClient(sender); - const guild = client.guilds.get(server); + const guild = await client.guilds.resolve(server); if (!guild) { throw new Error(`Guild "${server}" not found`); } - const channel = guild.channels.get(room); + const channel = await guild.channels.resolve(room); if (channel && channel.type === "text") { if (hasSender) { - const permissions = channel.permissionsFor(guild.me); + const permissions = guild.me && channel.permissionsFor(guild.me); if (!permissions || !permissions.has("VIEW_CHANNEL") || !permissions.has("SEND_MESSAGES")) { throw new Error(`Can't send into channel`); } @@ -363,8 +381,8 @@ export class DiscordBot { this.ClientFactory.bindMetricsToChannel(channel as Discord.TextChannel); const lookupResult = new ChannelLookupResult(); lookupResult.channel = channel as Discord.TextChannel; - lookupResult.botUser = this.bot.user.id === client.user.id; - lookupResult.canSendEmbeds = client.user.bot; // only bots can send embeds + lookupResult.botUser = this.BotUserId === client.user?.id; + lookupResult.canSendEmbeds = client.user?.bot || false; // only bots can send embeds return lookupResult; } throw new Error(`Channel "${room}" not found`); @@ -402,7 +420,7 @@ export class DiscordBot { const chan = roomLookup.channel; const botUser = roomLookup.botUser; const embed = embedSet.messageEmbed; - const oldMsg = await chan.fetchMessage(editEventId); + const oldMsg = await chan.messages.fetch(editEventId); if (!oldMsg) { // old message not found, just sending this normally await this.send(embedSet, opts, roomLookup, event); @@ -476,14 +494,16 @@ export class DiscordBot { let hook: Discord.Webhook | undefined; if (botUser) { const webhooks = await chan.fetchWebhooks(); - hook = webhooks.filterArray((h) => h.name === "_matrix").pop(); + hook = webhooks.filter((h) => h.name === "_matrix").first(); // 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"); + { + avatar: MATRIX_ICON_URL, + reason: "Matrix Bridge: Allow rich user messages" + }); } } catch (err) { // throw wrapError(err, Unstable.ForeignNetworkError, "Unable to create \"_matrix\" webhook"); @@ -502,11 +522,11 @@ export class DiscordBot { MetricPeg.get.remoteCall("hook.send"); const embeds = this.prepareEmbedSetWebhook(embedSet); msg = await hook.send(embed.description, { - avatarURL: embed!.author!.icon_url, + avatarURL: embed!.author!.iconURL, embeds, - files: opts.file ? [opts.file] : undefined, + files: opts.files, username: embed!.author!.name, - } as Discord.WebhookMessageOptions); + }); } else { opts.embed = this.prepareEmbedSetBot(embedSet); msg = await chan.send("", opts); @@ -547,7 +567,7 @@ export class DiscordBot { const result = await this.LookupRoom(storeEvent.GuildId, storeEvent.ChannelId, event.sender); const chan = result.channel; - const msg = await chan.fetchMessage(storeEvent.DiscordId); + const msg = await chan.messages.fetch(storeEvent.DiscordId); try { this.channelLock.set(msg.channel.id); await msg.delete(); @@ -567,10 +587,11 @@ export class DiscordBot { userId: Discord.Snowflake, guildId?: Discord.Snowflake, ): Promise<Discord.User|Discord.GuildMember|undefined> { try { - if (guildId && this.bot.guilds.has(guildId)) { - return await this.bot.guilds.get(guildId)!.fetchMember(userId); + const guild = guildId && await this.bot.guilds.fetch(guildId); + if (guild) { + return await guild.members.fetch(userId); } - return await this.bot.fetchUser(userId); + return await this.bot.users.fetch(userId); } catch (ex) { log.warn(`Could not fetch user data for ${userId} (guild: ${guildId})`); return undefined; @@ -594,9 +615,9 @@ export class DiscordBot { if (!entry.remote) { throw Error("Room had no remote component"); } - const guild = client.guilds.get(entry.remote!.get("discord_guild") as string); + const guild = await client.guilds.fetch(entry.remote!.get("discord_guild") as string); if (guild) { - const channel = client.channels.get(entry.remote!.get("discord_channel") as string); + const channel = await client.channels.fetch(entry.remote!.get("discord_channel") as string); if (channel) { this.ClientFactory.bindMetricsToChannel(channel as Discord.TextChannel); return channel; @@ -640,7 +661,7 @@ export class DiscordBot { if (member) { let rooms: string[] = []; - await Util.AsyncForEach(guild.channels.array(), async (channel) => { + await Util.AsyncForEach(guild.channels.cache.array(), async (channel) => { if (channel.type !== "text" || !channel.members.has(member.id)) { return; } @@ -698,13 +719,13 @@ export class DiscordBot { let res: Discord.Message; const botChannel = await this.GetChannelFromRoomId(roomId) as Discord.TextChannel; if (restore) { - await tchan.overwritePermissions(kickee, + await tchan.overwritePermissions([ { - SEND_MESSAGES: null, - VIEW_CHANNEL: null, - /* tslint:disable-next-line no-any */ - } as any, // XXX: Discord.js typings are wrong. - `Unbanned.`); + id: kickee.id, + allow: ['SEND_MESSAGES', 'VIEW_CHANNEL'] + }], + `Unbanned.` + ); this.channelLock.set(botChannel.id); res = await botChannel.send( `${kickee} was unbanned from this channel by ${kicker}.`, @@ -713,7 +734,7 @@ export class DiscordBot { this.channelLock.release(botChannel.id); return; } - const existingPerms = tchan.memberPermissions(kickee); + const existingPerms = tchan.permissionsFor(kickee); if (existingPerms && existingPerms.has(Discord.Permissions.FLAGS.VIEW_CHANNEL as number) === false ) { log.warn("User isn't allowed to read anyway."); return; @@ -728,23 +749,24 @@ export class DiscordBot { this.channelLock.release(botChannel.id); log.info(`${word} ${kickee}`); - await tchan.overwritePermissions(kickee, + await tchan.overwritePermissions([ { - SEND_MESSAGES: false, - VIEW_CHANNEL: false, - }, - `Matrix user was ${word} by ${kicker}`); + id: kickee.id, + deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'] + }], + `Matrix user was ${word} by ${kicker}.` + ); if (kickban === "leave") { // Kicks will let the user back in after ~30 seconds. setTimeout(async () => { log.info(`Kick was lifted for ${kickee.displayName}`); - await tchan.overwritePermissions(kickee, + await tchan.overwritePermissions([ { - SEND_MESSAGES: null, - VIEW_CHANNEL: null, - /* tslint:disable: no-any */ - } as any, // XXX: Discord.js typings are wrong. - `Lifting kick since duration expired.`); + id: kickee.id, + deny: ['SEND_MESSAGES', 'VIEW_CHANNEL'] + }], + `Lifting kick since duration expired.` + ); }, this.config.room.kickFor); } } @@ -768,11 +790,11 @@ export class DiscordBot { return embed.description += addText; } - private prepareEmbedSetBotAccount(embedSet: IMatrixEventProcessorResult): Discord.RichEmbed | undefined { + private prepareEmbedSetBotAccount(embedSet: IMatrixEventProcessorResult): Discord.MessageEmbed | undefined { if (!embedSet.imageEmbed && !embedSet.replyEmbed) { return undefined; } - let sendEmbed = new Discord.RichEmbed(); + let sendEmbed = new Discord.MessageEmbed(); if (embedSet.imageEmbed) { if (!embedSet.replyEmbed) { sendEmbed = embedSet.imageEmbed; @@ -791,8 +813,8 @@ export class DiscordBot { return sendEmbed; } - private prepareEmbedSetWebhook(embedSet: IMatrixEventProcessorResult): Discord.RichEmbed[] { - const embeds: Discord.RichEmbed[] = []; + private prepareEmbedSetWebhook(embedSet: IMatrixEventProcessorResult): Discord.MessageEmbed[] { + const embeds: Discord.MessageEmbed[] = []; if (embedSet.imageEmbed) { embeds.push(embedSet.imageEmbed); } @@ -802,7 +824,7 @@ export class DiscordBot { return embeds; } - private prepareEmbedSetBot(embedSet: IMatrixEventProcessorResult): Discord.RichEmbed { + private prepareEmbedSetBot(embedSet: IMatrixEventProcessorResult): Discord.MessageEmbed { const embed = embedSet.messageEmbed; if (embedSet.imageEmbed) { embed.setImage(embedSet.imageEmbed.image!.url); @@ -840,7 +862,7 @@ export class DiscordBot { return true; } - private async OnTyping(channel: Discord.Channel, user: Discord.User, isTyping: boolean) { + private async OnTyping(channel: Discord.Channel, user: Discord.User|Discord.PartialUser, isTyping: boolean) { const rooms = await this.channelSync.GetRoomIdsFromChannel(channel); try { const intent = this.GetIntentFromDiscordMember(user); @@ -848,6 +870,16 @@ export class DiscordBot { await Promise.all(rooms.map( async (roomId) => { return intent.underlyingClient.setTyping(roomId, isTyping); })); + const typingKey = `${user.id}:${channel.id}`; + if (isTyping) { + if (this.typingTimers[typingKey]) { + clearTimeout(this.typingTimers[typingKey]); + } + this.typingTimers[typingKey] = setTimeout(async () => { + this.OnTyping(channel, user, false); + delete this.typingTimers[typingKey]; + }, TYPING_TIMEOUT_MS); + } } catch (err) { log.warn("Failed to send typing indicator.", err); } @@ -862,7 +894,7 @@ export class DiscordBot { return; // Skip *our* messages } const chan = msg.channel as Discord.TextChannel; - if (msg.author.id === this.bot.user.id) { + if (msg.author.id === this.BotUserId) { // We don't support double bridging. log.verbose("Not reflecting bot's own messages"); MetricPeg.get.requestOutcome(msg.id, true, "dropped"); @@ -871,7 +903,7 @@ export class DiscordBot { // Test for webhooks if (msg.webhookID) { const webhook = (await chan.fetchWebhooks()) - .filterArray((h) => h.name === "_matrix").pop(); + .filter((h) => h.name === "_matrix").first(); if (webhook && msg.webhookID === webhook.id) { // Filter out our own webhook messages. log.verbose("Not reflecting own webhook messages"); @@ -947,7 +979,9 @@ export class DiscordBot { evt.MatrixId = `${eventId};${room}`; evt.DiscordId = msg.id; evt.ChannelId = msg.channel.id; - evt.GuildId = msg.guild.id; + if (msg.guild) { + evt.GuildId = msg.guild.id; + } await this.store.Insert(evt); }); }); @@ -987,7 +1021,9 @@ export class DiscordBot { evt.MatrixId = `${eventId};${room}`; evt.DiscordId = msg.id; evt.ChannelId = msg.channel.id; - evt.GuildId = msg.guild.id; + if (msg.guild) { + evt.GuildId = msg.guild.id; + } await this.store.Insert(evt); }; let res; diff --git a/src/channelsyncroniser.ts b/src/channelsyncroniser.ts index 2877f3d..f03ef3d 100644 --- a/src/channelsyncroniser.ts +++ b/src/channelsyncroniser.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { DiscordBot } from "./bot"; import { Util } from "./util"; import { DiscordBridgeConfig, DiscordBridgeConfigChannelDeleteOptions } from "./config"; @@ -81,7 +81,7 @@ export class ChannelSyncroniser { public async OnGuildUpdate(guild: Discord.Guild, force = false) { log.verbose(`Got guild update for guild ${guild.id}`); const channelStates: IChannelState[] = []; - for (const [_, channel] of guild.channels) { + for (const [_, channel] of guild.channels.cache) { if (channel.type !== "text") { continue; // not supported for now } @@ -144,7 +144,7 @@ export class ChannelSyncroniser { } public async OnGuildDelete(guild: Discord.Guild) { - for (const [_, channel] of guild.channels) { + for (const [_, channel] of guild.channels.cache) { try { await this.OnDelete(channel); } catch (e) { diff --git a/src/clientfactory.ts b/src/clientfactory.ts index 1b2adcf..8379840 100644 --- a/src/clientfactory.ts +++ b/src/clientfactory.ts @@ -16,7 +16,7 @@ limitations under the License. import { DiscordBridgeConfigAuth } from "./config"; import { DiscordStore } from "./store"; -import { Client as DiscordClient, TextChannel } from "discord.js"; +import { Client as DiscordClient, TextChannel } from "better-discord.js" import { Log } from "./log"; import { MetricPeg } from "./metrics"; @@ -42,11 +42,10 @@ export class DiscordClientFactory { this.botClient = new DiscordClient({ fetchAllMembers: true, messageCacheLifetime: 5, - sync: true, }); try { - await this.botClient.login(this.config.botToken); + await this.botClient.login(this.config.botToken, true); } catch (err) { log.error("Could not login as the bot user. This is bad!", err); throw err; @@ -58,16 +57,14 @@ export class DiscordClientFactory { const client = new DiscordClient({ fetchAllMembers: false, messageCacheLifetime: 5, - sync: false, }); - await client.login(token); - const id = client.user.id; - - // This can be done asynchronously, because we don't need to block to return the id. - client.destroy().catch((err) => { - log.warn("Failed to destroy client ", id); - }); + await client.login(token, false); + const id = client.user?.id; + client.destroy(); + if (!id) { + throw Error('Client did not have a user object, cannot determine ID'); + } return id; } @@ -90,7 +87,6 @@ export class DiscordClientFactory { const client = new DiscordClient({ fetchAllMembers: true, messageCacheLifetime: 5, - sync: true, }); const jsLog = new Log("discord.js-ppt"); @@ -99,7 +95,7 @@ export class DiscordClientFactory { client.on("warn", (msg) => { jsLog.warn(msg); }); try { - await client.login(token); + await client.login(token, false); log.verbose("Logged in. Storing ", userId); this.clients.set(userId, client); return client; diff --git a/src/db/schema/dbschema.ts b/src/db/schema/dbschema.ts index 132e7c0..9f215f1 100644 --- a/src/db/schema/dbschema.ts +++ b/src/db/schema/dbschema.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { DiscordStore } from "../../store"; -import { DiscordBridgeConfigDatabase } from "../../config"; export interface IDbSchema { description: string; run(store: DiscordStore): Promise<null|void|Error|Error[]>; diff --git a/src/db/schema/v3.ts b/src/db/schema/v3.ts index a24a139..f8c1726 100644 --- a/src/db/schema/v3.ts +++ b/src/db/schema/v3.ts @@ -78,8 +78,8 @@ directory.`); log.info("Moving ", row.userId); try { const client = await clientFactory.getClient(row.token); - const dId = client.user.id; - if (dId === null) { + const dId = client.user?.id; + if (!dId) { continue; } log.verbose("INSERT INTO discord_id_token."); diff --git a/src/db/schema/v8.ts b/src/db/schema/v8.ts index b9db732..575869a 100644 --- a/src/db/schema/v8.ts +++ b/src/db/schema/v8.ts @@ -17,8 +17,6 @@ limitations under the License. import {IDbSchema} from "./dbschema"; import {DiscordStore} from "../../store"; import { Log } from "../../log"; -import { RemoteStoreRoom, MatrixStoreRoom } from "../roomstore"; -const log = new Log("SchemaV8"); export class Schema implements IDbSchema { public description = "create room store tables"; diff --git a/src/db/schema/v9.ts b/src/db/schema/v9.ts index abb63e4..fc9e270 100644 --- a/src/db/schema/v9.ts +++ b/src/db/schema/v9.ts @@ -16,7 +16,6 @@ limitations under the License. import { IDbSchema } from "./dbschema"; import { DiscordStore } from "../../store"; -import { Log } from "../../log"; export class Schema implements IDbSchema { public description = "create user store tables"; diff --git a/src/discordcommandhandler.ts b/src/discordcommandhandler.ts index 7606383..978433b 100644 --- a/src/discordcommandhandler.ts +++ b/src/discordcommandhandler.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { DiscordBot } from "./bot"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util"; import { Log } from "./log"; import { Appservice } from "matrix-bot-sdk"; @@ -34,6 +34,12 @@ export class DiscordCommandHandler { await msg.channel.send("**ERROR:** only available for guild channels"); return; } + if (!msg.member) { + await msg.channel.send("**ERROR:** could not determine message member"); + return; + } + + const discordMember = msg.member; const intent = this.bridge.botIntent; @@ -43,7 +49,7 @@ export class DiscordCommandHandler { params: [], permission: "MANAGE_WEBHOOKS", run: async () => { - if (await this.discord.Provisioner.MarkApproved(chan, msg.member, true)) { + if (await this.discord.Provisioner.MarkApproved(chan, discordMember, true)) { return "Thanks for your response! The matrix bridge has been approved"; } else { return "Thanks for your response, however" + @@ -62,7 +68,7 @@ export class DiscordCommandHandler { params: [], permission: "MANAGE_WEBHOOKS", run: async () => { - if (await this.discord.Provisioner.MarkApproved(chan, msg.member, false)) { + if (await this.discord.Provisioner.MarkApproved(chan, discordMember, false)) { return "Thanks for your response! The matrix bridge has been declined"; } else { return "Thanks for your response, however" + @@ -105,7 +111,7 @@ export class DiscordCommandHandler { if (!Array.isArray(permission)) { permission = [permission]; } - return permission.every((p) => msg.member.hasPermission(p as Discord.PermissionResolvable)); + return permission.every((p) => discordMember.hasPermission(p as Discord.PermissionResolvable)); }; const reply = await Util.ParseCommand("!matrix", msg.content, actions, parameters, permissionCheck); @@ -115,7 +121,7 @@ export class DiscordCommandHandler { private ModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: "kick"|"ban"|"unban") { return async ({name}) => { let allChannelMxids: string[] = []; - await Promise.all(discordChannel.guild.channels.map(async (chan) => { + await Promise.all(discordChannel.guild.channels.cache.map(async (chan) => { try { const chanMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(chan); allChannelMxids = allChannelMxids.concat(chanMxids); diff --git a/src/discordmessageprocessor.ts b/src/discordmessageprocessor.ts index 95cbc0d..13ed3d8 100644 --- a/src/discordmessageprocessor.ts +++ b/src/discordmessageprocessor.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { DiscordBot } from "./bot"; import { Log } from "./log"; import { @@ -54,7 +54,7 @@ export class DiscordMessageProcessor { private getParserCallbacks(msg: Discord.Message): IDiscordMessageParserCallbacks { return { getChannel: async (id: string) => { - const channel = msg.guild.channels.get(id); + const channel = await msg.guild?.channels.resolve(id); if (!channel) { return null; } @@ -77,7 +77,7 @@ export class DiscordMessageProcessor { return null; }, getUser: async (id: string) => { - const member = msg.guild.members.get(id); + const member = await msg.guild?.members.fetch(id); const mxid = `@_discord_${id}:${this.domain}`; const name = member ? member.displayName : mxid; return { diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts index 3d266b0..7af3957 100644 --- a/src/matrixcommandhandler.ts +++ b/src/matrixcommandhandler.ts @@ -20,7 +20,7 @@ import { DiscordBridgeConfig } from "./config"; import { IMatrixEvent } from "./matrixtypes"; import { Provisioner } from "./provisioner"; import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { Appservice } from "matrix-bot-sdk"; import * as markdown from "discord-markdown"; import { IRoomStoreEntry } from "./db/roomstore"; diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts index 388fe15..f1d66d3 100644 --- a/src/matrixeventprocessor.ts +++ b/src/matrixeventprocessor.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { DiscordBot } from "./bot"; import { DiscordBridgeConfig } from "./config"; import { Util, wrapError } from "./util"; @@ -54,9 +54,9 @@ export class MatrixEventProcessorOpts { } export interface IMatrixEventProcessorResult { - messageEmbed: Discord.RichEmbed; - replyEmbed?: Discord.RichEmbed; - imageEmbed?: Discord.RichEmbed; + messageEmbed: Discord.MessageEmbed; + replyEmbed?: Discord.MessageEmbed; + imageEmbed?: Discord.MessageEmbed; } export class MatrixEventProcessor { @@ -204,9 +204,9 @@ export class MatrixEventProcessor { if (typeof(file) === "string") { embedSet.messageEmbed.description += " " + file; } else if ((file as Discord.FileOptions).name && (file as Discord.FileOptions).attachment) { - opts.file = file as Discord.FileOptions; + opts.files = [file as Discord.FileOptions]; } else { - embedSet.imageEmbed = file as Discord.RichEmbed; + embedSet.imageEmbed = file as Discord.MessageEmbed; } // Throws an `Unstable.ForeignNetworkError` when sending the message fails. @@ -303,7 +303,7 @@ export class MatrixEventProcessor { body = await this.matrixMsgProcessor.FormatMessage(content as IMatrixMessage, channel.guild, params); } - const messageEmbed = new Discord.RichEmbed(); + const messageEmbed = new Discord.MessageEmbed(); messageEmbed.setDescription(body); await this.SetEmbedAuthor(messageEmbed, event.sender, profile); const replyEmbed = getReply ? (await this.GetEmbedForReply(event, channel)) : undefined; @@ -327,7 +327,7 @@ export class MatrixEventProcessor { event: IMatrixEvent, mxClient: MatrixClient, sendEmbeds: boolean = false, - ): Promise<string|Discord.FileOptions|Discord.RichEmbed> { + ): Promise<string|Discord.FileOptions|Discord.MessageEmbed> { if (!this.HasAttachment(event)) { return ""; } @@ -361,7 +361,7 @@ export class MatrixEventProcessor { } } if (sendEmbeds && event.content.info.mimetype.split("/")[0] === "image") { - return new Discord.RichEmbed() + return new Discord.MessageEmbed() .setImage(url); } return `[${name}](${url})`; @@ -370,7 +370,7 @@ export class MatrixEventProcessor { public async GetEmbedForReply( event: IMatrixEvent, channel: Discord.TextChannel, - ): Promise<Discord.RichEmbed|undefined> { + ): Promise<Discord.MessageEmbed|undefined> { if (!event.content) { event.content = {}; } @@ -416,7 +416,7 @@ export class MatrixEventProcessor { log.warn("Failed to handle reply, showing a unknown embed:", ex); } // For some reason we failed to get the event, so using fallback. - const embed = new Discord.RichEmbed(); + const embed = new Discord.MessageEmbed(); embed.setDescription("Reply with unknown content"); embed.setAuthor("Unknown"); return embed; @@ -487,7 +487,7 @@ export class MatrixEventProcessor { return hasAttachment; } - private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: { + private async SetEmbedAuthor(embed: Discord.MessageEmbed, sender: string, profile?: { displayname: string, avatar_url: string|undefined }) { let displayName = sender; @@ -500,13 +500,13 @@ export class MatrixEventProcessor { if (userOrMember instanceof Discord.User) { embed.setAuthor( userOrMember.username, - userOrMember.avatarURL, + userOrMember.avatarURL() || undefined, ); return; } else if (userOrMember instanceof Discord.GuildMember) { embed.setAuthor( userOrMember.displayName, - userOrMember.user.avatarURL, + userOrMember.user.avatarURL() || undefined, ); return; } diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts index eac3d67..2c855c5 100644 --- a/src/matrixmessageprocessor.ts +++ b/src/matrixmessageprocessor.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { IMatrixMessage } from "./matrixtypes"; import * as Parser from "node-html-parser"; import { Util } from "./util"; @@ -86,7 +86,8 @@ export class MatrixMessageProcessor { getChannelId: async (mxid: string) => { const CHANNEL_REGEX = /^#_discord_[0-9]*_([0-9]*):/; const match = mxid.match(CHANNEL_REGEX); - if (!match || !guild.channels.get(match[1])) { + const channel = match && await guild.channels.resolve(match[1]); + if (!channel) { /* This isn't formatted in #_discord_, so let's fetch the internal room ID and see if it is still a bridged room! @@ -103,26 +104,27 @@ export class MatrixMessageProcessor { } return null; } - return match[1]; + return match && match[1] || null; }, getEmoji: async (mxc: string, name: string) => { - let emoji: Discord.Emoji | null = null; + let emoji: {id: string, animated: boolean, name: string} | null = null; try { const emojiDb = await this.bot.GetEmojiByMxc(mxc); const id = emojiDb.EmojiId; - emoji = guild.emojis.find((e) => e.id === id); + emoji = await guild.emojis.resolve(id); } catch (e) { emoji = null; } if (!emoji) { - emoji = guild.emojis.find((e) => e.name === name); + emoji = await guild.emojis.resolve(name); } return emoji; }, getUserId: async (mxid: string) => { const USER_REGEX = /^@_discord_([0-9]*)/; const match = mxid.match(USER_REGEX); - if (!match || !guild.members.get(match[1])) { + const member = match && await guild.members.fetch(match[1]); + if (!match || member) { return null; } return match[1]; diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts index e101dde..ffe2252 100644 --- a/src/matrixroomhandler.ts +++ b/src/matrixroomhandler.ts @@ -17,7 +17,7 @@ limitations under the License. import { DiscordBot, IThirdPartyLookup } from "./bot"; import { DiscordBridgeConfig } from "./config"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { Util } from "./util"; import { Provisioner } from "./provisioner"; import { Log } from "./log"; diff --git a/src/presencehandler.ts b/src/presencehandler.ts index 6a8b602..7901880 100644 --- a/src/presencehandler.ts +++ b/src/presencehandler.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { User, Presence } from "discord.js"; +import { User, Presence } from "better-discord.js" import { DiscordBot } from "./bot"; import { Log } from "./log"; import { MetricPeg } from "./metrics"; @@ -32,7 +32,7 @@ interface IMatrixPresence { } export class PresenceHandler { - private presenceQueue: User[]; + private presenceQueue: Presence[]; private interval: NodeJS.Timeout | null; constructor(private bot: DiscordBot) { this.presenceQueue = []; @@ -62,17 +62,17 @@ export class PresenceHandler { this.interval = null; } - public EnqueueUser(user: User) { - if (user.id !== this.bot.GetBotId() && this.presenceQueue.find((u) => u.id === user.id) === undefined) { - log.verbose(`Adding ${user.id} (${user.username}) to the presence queue`); - this.presenceQueue.push(user); + public EnqueueUser(presence: Presence) { + if (presence.userID !== this.bot.GetBotId() && this.presenceQueue.find((u) => u.userID === presence.userID) === undefined) { + log.verbose(`Adding ${presence.userID} (${presence.user?.username}) to the presence queue`); + this.presenceQueue.push(presence); MetricPeg.get.setPresenceCount(this.presenceQueue.length); } } public DequeueUser(user: User) { const index = this.presenceQueue.findIndex((item) => { - return user.id === item.id; + return user.id === item.userID; }); if (index !== -1) { this.presenceQueue.splice(index, 1); @@ -84,20 +84,23 @@ export class PresenceHandler { } } - public async ProcessUser(user: User): Promise<boolean> { - const status = this.getUserPresence(user.presence); - await this.setMatrixPresence(user, status); + public async ProcessUser(presence: Presence): Promise<boolean> { + if (!presence.user) { + return true; + } + const status = this.getUserPresence(presence); + await this.setMatrixPresence(presence.user, status); return status.ShouldDrop; } private async processIntervalThread() { - const user = this.presenceQueue.shift(); - if (user) { - const proccessed = await this.ProcessUser(user); + const presence = this.presenceQueue.shift(); + if (presence) { + const proccessed = await this.ProcessUser(presence); if (!proccessed) { - this.presenceQueue.push(user); + this.presenceQueue.push(presence); } else { - log.verbose(`Dropping ${user.id} from the presence queue.`); + log.verbose(`Dropping ${presence.userID} from the presence queue.`); MetricPeg.get.setPresenceCount(this.presenceQueue.length); } } @@ -106,10 +109,13 @@ export class PresenceHandler { private getUserPresence(presence: Presence): PresenceHandlerStatus { const status = new PresenceHandlerStatus(); - if (presence.game) { - status.StatusMsg = `${presence.game.streaming ? "Streaming" : "Playing"} ${presence.game.name}`; - if (presence.game.url) { - status.StatusMsg += ` | ${presence.game.url}`; + // How do we show multiple activities? + if (presence.activities[0]) { + const activity = presence.activities[0]; + const type = activity.type[0] + activity.type.substring(1).toLowerCase(); // STREAMING -> Streaming; + status.StatusMsg = `${type} ${activity.name}`; + if (activity.url) { + status.StatusMsg += ` | ${activity.url}`; } } diff --git a/src/provisioner.ts b/src/provisioner.ts index 77cd3cc..0afb3d9 100644 --- a/src/provisioner.ts +++ b/src/provisioner.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { DbRoomStore, RemoteStoreRoom, MatrixStoreRoom } from "./db/roomstore"; import { ChannelSyncroniser } from "./channelsyncroniser"; import { Log } from "./log"; diff --git a/src/usersyncroniser.ts b/src/usersyncroniser.ts index 5633574..1249077 100644 --- a/src/usersyncroniser.ts +++ b/src/usersyncroniser.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { User, GuildMember } from "discord.js"; +import { User, GuildMember } from "better-discord.js" import { DiscordBot } from "./bot"; import { Util } from "./util"; import { DiscordBridgeConfig } from "./config"; @@ -256,8 +256,10 @@ export class UserSyncroniser { log.verbose(`Could not find user in remote user store.`); userState.createUser = true; userState.displayName = displayName; - userState.avatarUrl = discordUser.avatarURL; - userState.avatarId = discordUser.avatar; + if (discordUser.avatar) { + userState.avatarUrl = discordUser.avatarURL(); + userState.avatarId = discordUser.avatar; + } return userState; } @@ -268,13 +270,13 @@ export class UserSyncroniser { } const oldAvatarUrl = remoteUser.avatarurl; - if (oldAvatarUrl !== discordUser.avatarURL) { + if (oldAvatarUrl !== discordUser.avatarURL()) { log.verbose(`User ${discordUser.id} avatarurl should be updated`); - if (discordUser.avatarURL !== null) { - userState.avatarUrl = discordUser.avatarURL; + if (discordUser.avatar) { + userState.avatarUrl = discordUser.avatarURL(); userState.avatarId = discordUser.avatar; } else { - userState.removeAvatar = oldAvatarUrl !== null; + userState.removeAvatar = userState.avatarId === null; } } @@ -296,7 +298,7 @@ export class UserSyncroniser { displayName: name, id: newMember.id, mxUserId: `@_discord_${newMember.id}:${this.config.bridge.domain}`, - roles: newMember.roles.map((role) => { return { + roles: newMember.roles.cache.map((role) => { return { color: role.color, name: role.name, position: role.position, diff --git a/src/util.ts b/src/util.ts index 46b30d5..e9edffb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -17,7 +17,7 @@ limitations under the License. import * as http from "http"; import * as https from "https"; import { Buffer } from "buffer"; -import { Permissions } from "discord.js"; +import { Permissions } from "better-discord.js" import { DiscordBridgeConfig } from "./config"; import { IMatrixEvent } from "./matrixtypes"; @@ -108,8 +108,7 @@ export class Util { public static GetBotLink(config: DiscordBridgeConfig): string { /* tslint:disable:no-bitwise */ - const perms = Permissions.FLAGS.READ_MESSAGES! | - Permissions.FLAGS.SEND_MESSAGES! | + const perms = Permissions.FLAGS.SEND_MESSAGES! | Permissions.FLAGS.CHANGE_NICKNAME! | Permissions.FLAGS.CONNECT! | Permissions.FLAGS.SPEAK! | diff --git a/test/mocks/channel.ts b/test/mocks/channel.ts index 438bdc5..f209cc2 100644 --- a/test/mocks/channel.ts +++ b/test/mocks/channel.ts @@ -16,7 +16,8 @@ limitations under the License. import {MockMember} from "./member"; import {MockCollection} from "./collection"; -import {Permissions, PermissionResolvable} from "discord.js"; +import {Permissions, PermissionResolvable, TextChannel} from "better-discord.js" +import { MockGuild } from "./guild"; // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ @@ -40,3 +41,16 @@ export class MockChannel { return new Permissions(Permissions.FLAGS.MANAGE_WEBHOOKS as PermissionResolvable); } } + +export class MockTextChannel extends TextChannel { + constructor(guild?: MockGuild, channelData: any = {}) { + // Mock the nessacery + super(guild || { + client: { + options: { + messageCacheMaxSize: -1, + } + } + } as any, channelData); + } +} diff --git a/test/mocks/collection.ts b/test/mocks/collection.ts index ad83045..d83bfe3 100644 --- a/test/mocks/collection.ts +++ b/test/mocks/collection.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Collection } from "discord.js"; +import { Collection } from "better-discord.js" // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ diff --git a/test/mocks/guild.ts b/test/mocks/guild.ts index 228decc..9f1c394 100644 --- a/test/mocks/guild.ts +++ b/test/mocks/guild.ts @@ -17,7 +17,7 @@ limitations under the License. import {MockCollection} from "./collection"; import {MockMember} from "./member"; import {MockEmoji} from "./emoji"; -import {Channel} from "discord.js"; +import {Channel} from "better-discord.js" import {MockRole} from "./role"; // we are a test file and thus need those @@ -39,6 +39,14 @@ export class MockGuild { }); } + public get client() { + return { + options: { + messageCacheMaxSize: -1, + } + }; + } + public async fetchMember(id: string): Promise<MockMember|Error> { if (this.members.has(id)) { return this.members.get(id)!; diff --git a/test/mocks/member.ts b/test/mocks/member.ts index df3b22c..8e9c06c 100644 --- a/test/mocks/member.ts +++ b/test/mocks/member.ts @@ -17,7 +17,7 @@ limitations under the License. import {MockCollection} from "./collection"; import {MockUser} from "./user"; import {MockRole} from "./role"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ @@ -30,7 +30,11 @@ export class MockMember { public roles = new MockCollection<string, MockRole>(); constructor(id: string, username: string, public guild: any = null, public displayName: string = username) { this.id = id; - this.presence = new Discord.Presence({}, {} as any); + this.presence = new Discord.Presence({} as any, { + user: { + id: this.id, + } + }); this.user = new MockUser(this.id, username); this.nickname = displayName; } diff --git a/test/mocks/message.ts b/test/mocks/message.ts index 148a743..44b2aa6 100644 --- a/test/mocks/message.ts +++ b/test/mocks/message.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { MockUser } from "./user"; import { MockCollection } from "./collection"; diff --git a/test/mocks/user.ts b/test/mocks/user.ts index ef12713..83c6f1f 100644 --- a/test/mocks/user.ts +++ b/test/mocks/user.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Presence } from "discord.js"; +import { Presence } from "better-discord.js" // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ diff --git a/test/test_clientfactory.ts b/test/test_clientfactory.ts index eb9ab99..ad2a29d 100644 --- a/test/test_clientfactory.ts +++ b/test/test_clientfactory.ts @@ -22,7 +22,7 @@ import { DiscordBridgeConfigAuth } from "../src/config"; /* tslint:disable:no-unused-expression max-file-line-count no-any */ const DiscordClientFactory = Proxyquire("../src/clientfactory", { - "discord.js": { Client: require("./mocks/discordclient").MockDiscordClient }, + "better-discord.js": { Client: require("./mocks/discordclient").MockDiscordClient }, }).DiscordClientFactory; const STORE = { diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts index f8e6e18..f2d21ad 100644 --- a/test/test_discordbot.ts +++ b/test/test_discordbot.ts @@ -16,7 +16,7 @@ limitations under the License. import { expect } from "chai"; import * as Proxyquire from "proxyquire"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { MockGuild } from "./mocks/guild"; import { MockMember } from "./mocks/member"; @@ -25,6 +25,7 @@ import { MockMessage } from "./mocks/message"; import { Util } from "../src/util"; import { AppserviceMock } from "./mocks/appservicemock"; import { MockUser } from "./mocks/user"; +import { MockTextChannel } from "./mocks/channel"; // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ @@ -103,7 +104,7 @@ describe("DiscordBot", () => { }); }); describe("OnMessage()", () => { - const channel = new Discord.TextChannel({} as any, {} as any); + const channel = new MockTextChannel(); const msg = new MockMessage(channel); const author = new MockUser("11111"); let HANDLE_COMMAND = false; @@ -291,7 +292,7 @@ describe("DiscordBot", () => { const guild: any = new MockGuild("123", []); guild._mockAddMember(new MockMember("12345", "TestUsername")); - const channel = new Discord.TextChannel(guild, {} as any); + const channel = new MockTextChannel(guild); const oldMsg = new MockMessage(channel) as any; const newMsg = new MockMessage(channel) as any; oldMsg.embeds = []; @@ -318,7 +319,7 @@ describe("DiscordBot", () => { const guild: any = new MockGuild("123", []); guild._mockAddMember(new MockMember("12345", "TestUsername")); - const channel = new Discord.TextChannel(guild, {} as any); + const channel = new MockTextChannel(guild); const oldMsg = new MockMessage(channel) as any; const newMsg = new MockMessage(channel) as any; oldMsg.embeds = []; @@ -362,7 +363,7 @@ describe("DiscordBot", () => { const guild: any = new MockGuild("123", []); guild._mockAddMember(new MockMember("12345", "TestUsername")); - const channel = new Discord.TextChannel(guild, {} as any); + const channel = new MockTextChannel(guild, {} as any); const oldMsg = new MockMessage(channel) as any; const newMsg = new MockMessage(channel) as any; oldMsg.embeds = []; diff --git a/test/test_discordmessageprocessor.ts b/test/test_discordmessageprocessor.ts index 88507a3..b6e793a 100644 --- a/test/test_discordmessageprocessor.ts +++ b/test/test_discordmessageprocessor.ts @@ -15,12 +15,13 @@ limitations under the License. */ import * as Chai from "chai"; // TODO: Use expect -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { DiscordMessageProcessor } from "../src/discordmessageprocessor"; import { DiscordBot } from "../src/bot"; import { MockGuild } from "./mocks/guild"; import { MockMember } from "./mocks/member"; import { MockMessage } from "./mocks/message"; +import { MockTextChannel } from "./mocks/channel"; // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ @@ -128,7 +129,7 @@ describe("DiscordMessageProcessor", () => { const processor = new DiscordMessageProcessor( "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {}); + const channel = new MockTextChannel(guild); const msg = new MockMessage(channel) as any; msg.embeds = []; msg.content = "<@12345>"; @@ -142,7 +143,7 @@ describe("DiscordMessageProcessor", () => { "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); guild._mockAddMember(new MockMember("12345", "TestUsername")); - const channel = new Discord.TextChannel(guild, {}); + const channel = new MockTextChannel(guild); const msg = new MockMessage(channel) as any; msg.embeds = []; msg.content = "<@12345>"; @@ -156,7 +157,7 @@ describe("DiscordMessageProcessor", () => { "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); guild._mockAddMember(new MockMember("12345", "TestUsername", null, "TestNickname")); - const channel = new Discord.TextChannel(guild, {}); + const channel = new MockTextChannel(guild); const msg = new MockMessage(channel) as any; msg.embeds = []; msg.content = "<@12345>"; @@ -171,7 +172,7 @@ describe("DiscordMessageProcessor", () => { const processor = new DiscordMessageProcessor( "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"}); const msg = new MockMessage(channel) as any; msg.embeds = []; msg.content = "Hello <:hello:123456789>"; @@ -183,7 +184,7 @@ describe("DiscordMessageProcessor", () => { const processor = new DiscordMessageProcessor( "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"}); guild.channels.set("456", channel); const msg = new MockMessage(channel) as any; msg.embeds = []; @@ -199,7 +200,7 @@ describe("DiscordMessageProcessor", () => { const processor = new DiscordMessageProcessor( "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"}); guild.channels.set("456", channel); const msg = new MockMessage(channel) as any; msg.embeds = []; @@ -212,7 +213,7 @@ describe("DiscordMessageProcessor", () => { const processor = new DiscordMessageProcessor( "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {id: "456", name: "TestChannel"}); + const channel = new MockTextChannel(guild, {id: "456", name: "TestChannel"}); guild.channels.set("456", channel); const msg = new MockMessage(channel) as any; msg.embeds = []; @@ -226,7 +227,7 @@ describe("DiscordMessageProcessor", () => { const processor = new DiscordMessageProcessor( "localhost", bot as DiscordBot); const guild: any = new MockGuild("123", []); - const channel = new Discord.TextChannel(guild, {id: "678", name: "TestChannel"}); + const channel = new MockTextChannel(guild, {id: "678", name: "TestChannel"}); guild.channels.set("678", channel); const msg = new MockMessage(channel) as any; msg.embeds = []; diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts index b002480..7d806eb 100644 --- a/test/test_matrixeventprocessor.ts +++ b/test/test_matrixeventprocessor.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { expect } from "chai"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import * as Proxyquire from "proxyquire"; import { MockMember } from "./mocks/member"; import { MatrixEventProcessor, MatrixEventProcessorOpts } from "../src/matrixeventprocessor"; @@ -265,7 +265,7 @@ describe("MatrixEventProcessor", () => { processor.HandleAttachment = async () => ""; processor.EventToEmbed = async (evt, chan) => { return { - messageEmbed: new Discord.RichEmbed(), + messageEmbed: new Discord.MessageEmbed(), }; }; const room = { data: { @@ -298,7 +298,7 @@ describe("MatrixEventProcessor", () => { processor.HandleAttachment = async () => ""; processor.EventToEmbed = async (evt, chan) => { return { - messageEmbed: new Discord.RichEmbed(), + messageEmbed: new Discord.MessageEmbed(), }; }; const room = { data: { @@ -435,7 +435,7 @@ describe("MatrixEventProcessor", () => { } as IMatrixEvent, mockChannel as any); const author = embeds.messageEmbed.author; expect(author!.name).to.equal("Test User"); - expect(author!.icon_url).to.equal("https://localhost/avatarurl"); + expect(author!.iconURL).to.equal("https://localhost/avatarurl"); expect(author!.url).to.equal("https://matrix.to/#/@test:localhost"); }); @@ -449,7 +449,7 @@ describe("MatrixEventProcessor", () => { } as IMatrixEvent, mockChannel as any); const author = embeds.messageEmbed.author; expect(author!.name).to.equal("Test User"); - expect(author!.icon_url).to.equal("https://localhost/avatarurl"); + expect(author!.iconURL).to.equal("https://localhost/avatarurl"); expect(author!.url).to.equal("https://matrix.to/#/@test:localhost"); }); @@ -463,7 +463,7 @@ describe("MatrixEventProcessor", () => { } as IMatrixEvent, mockChannel as any); const author = embeds.messageEmbed.author; expect(author!.name).to.equal("@test_nonexistant:localhost"); - expect(author!.icon_url).to.be.undefined; + expect(author!.iconURL).to.be.undefined; expect(author!.url).to.equal("https://matrix.to/#/@test_nonexistant:localhost"); }); @@ -513,7 +513,7 @@ describe("MatrixEventProcessor", () => { } as IMatrixEvent, mockChannel as any); const author = embeds.messageEmbed.author; expect(author!.name).to.equal("Test User"); - expect(author!.icon_url).to.equal("https://localhost/avatarurl"); + expect(author!.iconURL).to.equal("https://localhost/avatarurl"); expect(author!.url).to.equal("https://matrix.to/#/@test:localhost"); }); @@ -603,7 +603,11 @@ describe("MatrixEventProcessor", () => { }, } as IMatrixEvent, realBridge.botIntent.underlyingClient)) as Discord.FileOptions; expect(attachment.name).to.eq("filename.webm"); - expect(attachment.attachment.length).to.eq(SMALL_FILE); + if (attachment.attachment instanceof Buffer) { + expect(attachment.attachment.length).to.eq(SMALL_FILE); + } else { + throw Error('Expected attachment to be a buffer'); + } }); it("message without a url", async () => { const {processor, realBridge} = createMatrixEventProcessor(); @@ -645,7 +649,11 @@ describe("MatrixEventProcessor", () => { }, } as IMatrixEvent, realBridge.botIntent.underlyingClient)) as Discord.FileOptions; expect(attachment.name).to.eq("filename.webm"); - expect(attachment.attachment.length).to.eq(SMALL_FILE); + if (attachment.attachment instanceof Buffer) { + expect(attachment.attachment.length).to.eq(SMALL_FILE); + } else { + throw Error('Expected attachment to be a buffer'); + } }); it("message with a small info.size but a larger file", async () => { const {processor, realBridge} = createMatrixEventProcessor(); @@ -675,7 +683,7 @@ describe("MatrixEventProcessor", () => { url: "mxc://localhost/8000000", }, } as IMatrixEvent, realBridge.botIntent.underlyingClient, true); - expect((ret as Discord.RichEmbed).image!.url).equals("https://localhost/8000000"); + expect((ret as Discord.MessageEmbed).image!.url).equals("https://localhost/8000000"); }); it("Should handle stickers.", async () => { const {processor, realBridge} = createMatrixEventProcessor(); @@ -722,7 +730,7 @@ describe("MatrixEventProcessor", () => { } as IMatrixEvent, mockChannel as any); expect(result!.description).to.be.equal("Hello!"); expect(result!.author!.name).to.be.equal("Doggo!"); - expect(result!.author!.icon_url).to.be.equal("https://fakeurl.com"); + expect(result!.author!.iconURL).to.be.equal("https://fakeurl.com"); expect(result!.author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost"); }); it("should handle replies with a missing event", async () => { @@ -744,7 +752,7 @@ This is where the reply goes`, } as IMatrixEvent, mockChannel as any); expect(result!.description).to.be.equal("Reply with unknown content"); expect(result!.author!.name).to.be.equal("Unknown"); - expect(result!.author!.icon_url).to.be.undefined; + expect(result!.author!.iconURL).to.be.undefined; expect(result!.author!.url).to.be.undefined; }); it("should handle replies with a valid reply event", async () => { @@ -766,7 +774,7 @@ This is where the reply goes`, } as IMatrixEvent, mockChannel as any); expect(result!.description).to.be.equal("Hello!"); expect(result!.author!.name).to.be.equal("Doggo!"); - expect(result!.author!.icon_url).to.be.equal("https://fakeurl.com"); + expect(result!.author!.iconURL).to.be.equal("https://fakeurl.com"); expect(result!.author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost"); }); it("should handle replies on top of replies", async () => { @@ -788,7 +796,7 @@ This is the second reply`, } as IMatrixEvent, mockChannel as any); expect(result!.description).to.be.equal("This is the first reply"); expect(result!.author!.name).to.be.equal("Doggo!"); - expect(result!.author!.icon_url).to.be.equal("https://fakeurl.com"); + expect(result!.author!.iconURL).to.be.equal("https://fakeurl.com"); expect(result!.author!.url).to.be.equal("https://matrix.to/#/@doggo:localhost"); }); it("should handle replies with non text events", async () => { @@ -810,7 +818,7 @@ This is the reply`, } as IMatrixEvent, mockChannel as any); expect(result!.description).to.be.equal("Reply with unknown content"); expect(result!.author!.name).to.be.equal("Unknown"); - expect(result!.author!.icon_url).to.be.undefined; + expect(result!.author!.iconURL).to.be.undefined; expect(result!.author!.url).to.be.undefined; }); it("should add the reply time", async () => { diff --git a/test/test_presencehandler.ts b/test/test_presencehandler.ts index b57c9b7..f11bae3 100644 --- a/test/test_presencehandler.ts +++ b/test/test_presencehandler.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { expect } from "chai"; -import * as Discord from "discord.js"; +import * as Discord from "better-discord.js" import { PresenceHandler } from "../src/presencehandler"; import { DiscordBot } from "../src/bot"; @@ -95,9 +95,10 @@ describe("PresenceHandler", () => { lastStatus = null; const handler = new PresenceHandler(bot as DiscordBot); const member = new MockUser("abc", "def") as any; - member.MockSetPresence(new Discord.Presence({ + member.MockSetPresence(new Discord.Presence({} as any, { status: "online", - }, {} as any)); + user: member, + })); await handler.ProcessUser(member); appservice.getIntentForSuffix(member.id) .underlyingClient.wasCalled("setPresenceStatus", true, "online", undefined); @@ -106,9 +107,10 @@ describe("PresenceHandler", () => { lastStatus = null; const handler = new PresenceHandler(bot as DiscordBot); const member = new MockUser("abc", "def") as any; - member.MockSetPresence(new Discord.Presence({ + member.MockSetPresence(new Discord.Presence({} as any, { status: "offline", - }, {} as any)); + user: member, + })); await handler.ProcessUser(member); appservice.getIntentForSuffix(member.id) .underlyingClient.wasCalled("setPresenceStatus", true, "offline", undefined); @@ -117,9 +119,10 @@ describe("PresenceHandler", () => { lastStatus = null; const handler = new PresenceHandler(bot as DiscordBot); const member = new MockUser("abc", "def") as any; - member.MockSetPresence(new Discord.Presence({ + member.MockSetPresence(new Discord.Presence({} as any, { status: "idle", - }, {} as any)); + user: member, + })); await handler.ProcessUser(member); appservice.getIntentForSuffix(member.id) .underlyingClient.wasCalled("setPresenceStatus", true, "unavailable", undefined); @@ -128,16 +131,18 @@ describe("PresenceHandler", () => { lastStatus = null; const handler = new PresenceHandler(bot as DiscordBot); const member = new MockUser("abc", "def") as any; - member.MockSetPresence(new Discord.Presence({ + member.MockSetPresence(new Discord.Presence({} as any, { status: "dnd", - }, {} as any)); + user: member, + })); await handler.ProcessUser(member); appservice.getIntentForSuffix(member.id) .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Do not disturb"); - member.MockSetPresence(new Discord.Presence({ - game: new Discord.Game({name: "Test Game"}, {} as any), + member.MockSetPresence(new Discord.Presence({} as any, { + activities: ({name: "Test Game", type: 'PLAYING'}), status: "dnd", - }, {} as any)); + user: member, + })); await handler.ProcessUser(member); appservice.getIntentForSuffix(member.id) .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Do not disturb | Playing Test Game"); @@ -146,17 +151,19 @@ describe("PresenceHandler", () => { lastStatus = null; const handler = new PresenceHandler(bot as DiscordBot); const member = new MockUser("abc", "def") as any; - member.MockSetPresence(new Discord.Presence({ - game: new Discord.Game({name: "Test Game"}, {} as any), + member.MockSetPresence(new Discord.Presence({} as any, { + activities: ({name: "Test Game", type: 'PLAYING'}), status: "online", - }, {} as any)); + user: member, + })); await handler.ProcessUser(member); appservice.getIntentForSuffix(member.id) .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Playing Test Game"); - member.MockSetPresence(new Discord.Presence({ - game: new Discord.Game({name: "Test Game", type: 1}, {} as any), + member.MockSetPresence(new Discord.Presence({} as any, { + activities: ({name: "Test Game", type: 'STREAMING'}), status: "online", - }, {} as any)); + user: member, + })); await handler.ProcessUser(member); appservice.getIntentForSuffix(member.id) .underlyingClient.wasCalled("setPresenceStatus", true, "online", "Streaming Test Game"); diff --git a/tools/chanfix.ts b/tools/chanfix.ts index 3e86a22..9b293e8 100644 --- a/tools/chanfix.ts +++ b/tools/chanfix.ts @@ -98,7 +98,7 @@ async function run() { const promiseList2: Promise<void>[] = []; let curDelay = config.limits.roomGhostJoinDelay; // we'll just re-use this - client.guilds.forEach((guild) => { + client.guilds.cache.forEach((guild) => { promiseList2.push((async () => { await Util.DelayedPromise(curDelay); try { diff --git a/tools/ghostfix.ts b/tools/ghostfix.ts index 1d0310b..88dee43 100644 --- a/tools/ghostfix.ts +++ b/tools/ghostfix.ts @@ -87,9 +87,9 @@ async function run() { const promiseList: Promise<void>[] = []; let curDelay = config.limits.roomGhostJoinDelay; try { - client.guilds.forEach((guild) => { - guild.members.forEach((member) => { - if (member.id === client.user.id) { + client.guilds.cache.forEach((guild) => { + guild.members.cache.forEach((member) => { + if (member.id === client.user?.id) { return; } promiseList.push((async () => { -- GitLab