diff --git a/src/bot.ts b/src/bot.ts index c2375e695c2f8a6f165848f340229c794053d4c3..59e4099de4f1d4c00d961e873aa017599ad1f66b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -6,6 +6,7 @@ import { DbEvent } from "./db/dbdataevent"; import { MatrixUser, RemoteUser, Bridge, Entry } from "matrix-appservice-bridge"; import { Util } from "./util"; import { MessageProcessor, MessageProcessorOpts } from "./messageprocessor"; +import { PresenceHandler } from "./presencehandler"; import * as Discord from "discord.js"; import * as log from "npmlog"; import * as Bluebird from "bluebird"; @@ -15,7 +16,7 @@ import * as path from "path"; // Due to messages often arriving before we get a response from the send call, // messages get delayed from discord. const MSG_PROCESS_DELAY = 750; -const PRESENCE_UPDATE_DELAY = 55000; // Synapse updates in 55 second intervals. +const PRESENCE_UPDATE_DELAY = 3000; class ChannelLookupResult { public channel: Discord.TextChannel; public botUser: boolean; @@ -30,6 +31,7 @@ export class DiscordBot { private presenceInterval: any; private sentMessages: string[]; private msgProcessor: MessageProcessor; + private presenceHandler: PresenceHandler; constructor(config: DiscordBridgeConfig, store: DiscordStore) { this.config = config; this.store = store; @@ -39,6 +41,7 @@ export class DiscordBot { new MessageProcessorOpts(this.config.bridge.domain), this, ); + this.presenceHandler = new PresenceHandler(this); } public setBridge(bridge: Bridge) { @@ -49,6 +52,10 @@ export class DiscordBot { return this.clientFactory; } + public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.User): any { + return this.bridge.getIntentFromLocalpart(`_discord_${member.id}`); + } + public run (): Promise<null> { return this.clientFactory.init().then(() => { return this.clientFactory.getClient(); @@ -58,7 +65,7 @@ export class DiscordBot { client.on("typingStop", (c, u) => { this.OnTyping(c, u, false); }); } if (!this.config.bridge.disablePresence) { - client.on("presenceUpdate", (_, newMember) => { this.UpdatePresence(newMember); }); + client.on("presenceUpdate", (_, newMember) => { this.presenceHandler.ProcessMember(newMember); }); } client.on("userUpdate", (_, newUser) => { this.UpdateUser(newUser); }); client.on("channelUpdate", (_, newChannel) => { this.UpdateRooms(newChannel); }); @@ -74,14 +81,12 @@ export class DiscordBot { this.bot = client; if (!this.config.bridge.disablePresence) { - /* Currently synapse sadly times out presence after a minute. - * This will set the presence for each user who is not offline */ - this.presenceInterval = setInterval( - this.BulkPresenceUpdate.bind(this), - PRESENCE_UPDATE_DELAY, - ); - this.BulkPresenceUpdate(); - return null; + this.bot.guilds.forEach((guild) =>{ + guild.members.forEach((member) => { + this.presenceHandler.EnqueueMember(member); + }) + }) + this.presenceHandler.Start(PRESENCE_UPDATE_DELAY); } }); } @@ -291,7 +296,7 @@ export class DiscordBot { } public InitJoinUser(member: Discord.GuildMember, roomIds: string[]): Promise<any> { - const intent = this.bridge.getIntentFromLocalpart(`_discord_${member.id}`); + const intent = this.GetIntentFromDiscordMember(member); return this.UpdateUser(member.user).then(() => { return Bluebird.each(roomIds, (roomId) => intent.join(roomId)); }).then(() => { @@ -441,47 +446,6 @@ export class DiscordBot { }); } - private BulkPresenceUpdate() { - if (this.config.bridge.disablePresence) { - return; // skip if there's nothing to do - } - - log.verbose("DiscordBot", "Bulk presence update"); - const members = []; - for (const guild of this.bot.guilds.values()) { - for (const member of guild.members.array().filter((m) => members.indexOf(m.id) === -1)) { - /* We ignore offline because they are likely to have been set - * by a 'presenceUpdate' event or will timeout. This saves - * some work on the HS */ - if (member.presence.status !== "offline") { - this.UpdatePresence(member); - } - members.push(member.id); - } - } -} - - private UpdatePresence(guildMember: Discord.GuildMember) { - if (this.config.bridge.disablePresence) { - return; // skip if there's nothing to do - } - - const intent = this.bridge.getIntentFromLocalpart(`_discord_${guildMember.id}`); - try { - const presence: any = {}; - presence.presence = guildMember.presence.status; - if (presence.presence === "idle" || presence.presence === "dnd") { - presence.presence = "unavailable"; - } - if (guildMember.presence.game) { - presence.status_msg = "Playing " + guildMember.presence.game.name; - } - intent.getClient().setPresence(presence); - } catch (err) { - log.info("DiscordBot", "Couldn't set presence ", err); - } - } - private AddGuildMember(guildMember: Discord.GuildMember) { return this.GetRoomIdsFromGuild(guildMember.guild.id).then((roomIds) => { return this.InitJoinUser(guildMember, roomIds); @@ -489,14 +453,15 @@ export class DiscordBot { } private RemoveGuildMember(guildMember: Discord.GuildMember) { - const intent = this.bridge.getIntentFromLocalpart(`_discord_${guildMember.id}`); + const intent = this.GetIntentFromDiscordMember(guildMember); return Bluebird.each(this.GetRoomIdsFromGuild(guildMember.guild.id), (roomId) => { + this.presenceHandler.DequeueMember(guildMember); return intent.leave(roomId); }); } private UpdateGuildMember(guildMember: Discord.GuildMember, roomIds?: string[]) { - const client = this.bridge.getIntentFromLocalpart(`_discord_${guildMember.id}`).getClient(); + const client = this.GetIntentFromDiscordMember(guildMember).getClient(); const userId = client.credentials.userId; let avatar = null; log.info(`Updating nick for ${guildMember.user.username}`); @@ -517,7 +482,7 @@ export class DiscordBot { private OnTyping(channel: Discord.Channel, user: Discord.User, isTyping: boolean) { this.GetRoomIdsFromChannel(channel).then((rooms) => { - const intent = this.bridge.getIntentFromLocalpart(`_discord_${user.id}`); + const intent = this.GetIntentFromDiscordMember(user); return Promise.all(rooms.map((room) => { return intent.sendTyping(room, isTyping); })); @@ -538,7 +503,6 @@ export class DiscordBot { return; } // Update presence because sometimes discord misses people. - this.UpdatePresence(msg.member); this.UpdateUser(msg.author).then(() => { return this.GetRoomIdsFromChannel(msg.channel).catch((err) => { log.verbose("DiscordBot", "No bridged rooms to send message to. Oh well."); @@ -548,7 +512,7 @@ export class DiscordBot { if (rooms === null) { return null; } - const intent = this.bridge.getIntentFromLocalpart(`_discord_${msg.author.id}`); + const intent = this.GetIntentFromDiscordMember(msg.author); // Check Attachements msg.attachments.forEach((attachment) => { Util.UploadContentFromUrl(attachment.url, intent, attachment.filename).then((content) => { @@ -607,7 +571,7 @@ export class DiscordBot { } while (storeEvent.Next()) { log.info("DiscordBot", `Deleting discord msg ${storeEvent.DiscordId}`); - const client = this.bridge.getIntent().getClient(); + const client = this.GetIntentFromDiscordMember(msg.author); const matrixIds = storeEvent.MatrixId.split(";"); await client.redactEvent(matrixIds[1], matrixIds[0]); } diff --git a/src/presencehandler.ts b/src/presencehandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8286988d279eae7fa587f571e8225f86d939c73 --- /dev/null +++ b/src/presencehandler.ts @@ -0,0 +1,102 @@ +import * as Discord from "discord.js"; +import * as log from "npmlog"; +import { DiscordBot } from "./bot"; + +export class PresenceHandlerStatus { + /* One of: ["online", "offline", "unavailable"] */ + public Presence: string; + public StatusMsg: string; + public ShouldDrop: boolean = false; +} + +export class PresenceHandler { + private readonly bot: DiscordBot; + private presenceQueue: Discord.GuildMember[]; + private interval: number; + constructor (bot: DiscordBot) { + this.bot = bot; + this.presenceQueue = new Array(); + } + + public Start(intervalTime: number) { + if (this.interval) { + log.info("PresenceHandler", "Restarting presence handler..."); + this.Stop(); + } + log.info("PresenceHandler", `Starting presence handler with new interval ${intervalTime}ms`); + this.interval = setInterval(this.processIntervalThread.bind(this), intervalTime); + } + + public Stop() { + if (!this.interval) { + log.info("PresenceHandler", "Can not stop interval, not running."); + } + log.info("PresenceHandler", "Stopping presence handler"); + clearInterval(this.interval); + this.interval = null; + } + + public EnqueueMember(member: Discord.GuildMember) { + if(!this.presenceQueue.includes(member)) { + log.info("PresenceHandler", `Adding ${member.id} (${member.user.username}) to the presence queue`); + this.presenceQueue.push(member); + } + } + + public DequeueMember(member: Discord.GuildMember) { + const index = this.presenceQueue.findIndex(member); + if(index !== -1) { + this.presenceQueue = this.presenceQueue.splice(this.presenceQueue.findIndex(member)); + } else { + log.warn("PresenceHandler", `Tried to remove ${member.id} from the presence queue but it could not be found`); + } + } + + public ProcessMember(member: Discord.GuildMember): boolean { + const status = this.getUserPresence(member.presence); + this.setMatrixPresence(member, status); + return status.ShouldDrop; + } + + private processIntervalThread() { + const item = this.presenceQueue.shift(); + if (item) { + if(!this.ProcessMember(item)) { + this.presenceQueue.push(item); + } else { + log.info("PresenceHandler", `Dropping ${member.id} from the presence queue.`); + } + } + } + + private getUserPresence(presence: Discord.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}`; + } + } + + if (presence.status === "online") { + status.Presence = "online"; + } else if (presence.status === "dnd") { + status.Presence = "online"; + status.StatusMsg = "Do not disturb | " + status.StatusMsg ? status.StatusMsg : ""; + } else if (presence.status === "offline") { + status.Presence = "offline"; + status.ShouldDrop = true; // Drop until we recieve an update. + } else { // idle or dnd + status.Presence = "unavailable"; + } + return status; + } + + private setMatrixPresence(guildMember: Discord.GuildMember, status: PresenceHandlerStatus) { + const intent = this.bot.GetIntentFromDiscordMember(guildMember); + intent.getClient().setPresence(status).catch((ex) => { + log.warn("PresenceHandler", `Could not update Matrix presence for ${guildMember.id}`); + }); + } +}