From 7d607b4dde896088397915bc0640abb0339dd45b Mon Sep 17 00:00:00 2001
From: Will Hunt <half-shot@molrams.com>
Date: Wed, 15 Mar 2017 17:26:39 +0000
Subject: [PATCH] Set presence regularly to avoid timing out users.

---
 README.md            |  4 +--
 src/bot.ts           | 63 +++++++++++++++++++++++++++++++++++---------
 src/clientfactory.ts | 11 +++++---
 3 files changed, 60 insertions(+), 18 deletions(-)

diff --git a/README.md b/README.md
index 5a572ab..74a5504 100644
--- a/README.md
+++ b/README.md
@@ -71,10 +71,10 @@ In a vague order of what is coming up next
   - [ ] Direct messages
  - [x] Rooms react to Discord updates
  - [ ] Integrate Discord into existing rooms.
- - [ ] Manage channel from Matrix
+ - [ ] Manage channel from Matrix (possibly)
   - [ ] Authorise admin rights from Discord to Matrix users
   - [ ] Topic
-  - [ ] Room Name (possibly)
+  - [ ] Room Name
  - [ ] Provisioning API
  - [ ] Webhooks (allows for prettier messages to discord)
  - [ ] VOIP (**Hard** | Unlikely to be finished anytime soon)
diff --git a/src/bot.ts b/src/bot.ts
index 7749cbf..354cb4b 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -15,7 +15,7 @@ import * as path from "path";
 // messages get delayed from discord.
 const MSG_PROCESS_DELAY = 750;
 const MATRIX_TO_LINK = "https://matrix.to/#/";
-
+const PRESENCE_UPDATE_DELAY = 60000; // Synapse updates in 30 intervals.
 class ChannelLookupResult {
   public channel: Discord.TextChannel;
   public botUser: boolean;
@@ -28,6 +28,7 @@ export class DiscordBot {
   private bot: Discord.Client;
   private discordUser: Discord.ClientUser;
   private bridge: Bridge;
+  private presenceInterval: any;
   private sentMessages: string[];
   constructor(config: DiscordBridgeConfig, store: DiscordStore) {
     this.config = config;
@@ -47,13 +48,21 @@ export class DiscordBot {
       client.on("typingStart", (c, u) => { this.OnTyping(c, u, true); });
       client.on("typingStop", (c, u) => { this.OnTyping(c, u, false); });
       client.on("userUpdate", (_, newUser) => { this.UpdateUser(newUser); });
-      client.on("channelUpdate", (_, newChannel) => { this.UpdateRooms(<Discord.TextChannel> newChannel); });
+      client.on("channelUpdate", (_, newChannel) => { this.UpdateRooms(newChannel); });
       client.on("presenceUpdate", (_, newMember) => { this.UpdatePresence(newMember); });
       client.on("message", (msg) => { Bluebird.delay(MSG_PROCESS_DELAY).then(() => {
           this.OnMessage(msg);
         });
       });
+      log.info("DiscordBot", "Discord bot client logged in.");
       this.bot = client;
+      /* 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;
     });
   }
@@ -82,7 +91,7 @@ export class DiscordBot {
         };
       });
     } else {
-      log.warn("DiscordBot", "Tried to do a third party lookup for a channel, but the guild did not exist");
+      log.info("DiscordBot", "Tried to do a third party lookup for a channel, but the guild did not exist");
       return [];
     }
   }
@@ -198,10 +207,15 @@ export class DiscordBot {
     });
   }
 
-  private UpdateRooms(discordChannel: Discord.TextChannel): Promise<null> {
+  private UpdateRooms(discordChannel: Discord.Channel): Promise<null> {
+    if (discordChannel.type !== "text") {
+      return; // Not supported for now.
+    }
+    log.info("DiscordBot", `Updating ${discordChannel.id}`);
+    const textChan = <Discord.TextChannel> discordChannel;
     const intent = this.bridge.getIntent();
     const roomStore = this.bridge.getRoomStore();
-    return this.GetRoomIdsFromChannel(discordChannel).then((rooms) => {
+    return this.GetRoomIdsFromChannel(textChan).then((rooms) => {
       return roomStore.getEntriesByMatrixIds(rooms).then( (entries) => {
         return Object.keys(entries).map((key) => entries[key]);
       });
@@ -210,7 +224,7 @@ export class DiscordBot {
         if (entry.length === 0) {
           return Promise.reject("Couldn't update room for channel, no assoicated entry in roomstore.");
         }
-        return this.UpdateRoomEntry(entry[0], discordChannel);
+        return this.UpdateRoomEntry(entry[0], textChan);
       }));
     });
   }
@@ -282,16 +296,31 @@ export class DiscordBot {
     });
   }
 
+  private BulkPresenceUpdate() {
+    log.info("DiscordBot", "Bulk presence update");
+    let members = [];
+    for (const guild of this.bot.guilds.values()) {
+      for (const member of guild.members.array().filter((m) => { return 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) {
-    log.info("DiscordBot", `Updating presence for ${guildMember.user.username}#${guildMember.user.discriminator}`);
     const intent = this.bridge.getIntentFromLocalpart(`_discord_${guildMember.id}`);
     try {
       let presence = guildMember.presence.status;
-      if (presence === "idle" || presence === "dnd") {
-        presence = "unavailable";
-      }
+      const msg = guildMember.presence.game ? "In Game: " + guildMember.presence.game : null;
+      presence = presence === "idle" || presence === "dnd" ? "unavailable" : presence;
       intent.getClient().setPresence({
         presence,
+        status_msg: msg,
       });
     } catch (err) {
       log.info("DiscordBot", "Couldn't set presence ", err);
@@ -344,8 +373,17 @@ export class DiscordBot {
       delete this.sentMessages[indexOfMsg];
       return; // Skip *our* messages
     }
+    if (msg.author.id === this.bot.user.id) {
+      // We don't support double bridging.
+      return;
+    }
+    // Update presence because sometimes discord misses people.
+    this.UpdatePresence(msg.member);
     this.UpdateUser(msg.author).then(() => {
-      return this.GetRoomIdsFromChannel(msg.channel);
+      return this.GetRoomIdsFromChannel(msg.channel).catch((err) => {
+        log.verbose("DiscordBot", "No bridged rooms to send message to. Oh well.");
+        throw err;
+      });
     }).then((rooms) => {
       const intent = this.bridge.getIntentFromLocalpart(`_discord_${msg.author.id}`);
       // Check Attachements
@@ -387,7 +425,8 @@ export class DiscordBot {
         });
       }
     }).catch((err) => {
-      log.warn("DiscordBot", "Failed to send message into room.", err);
+      // 99% of the time, this is because we haven't made the room yet.
+      log.verbose("DiscordBot", "Failed to send message into room.", err);
     });
   }
 }
diff --git a/src/clientfactory.ts b/src/clientfactory.ts
index f8e9958..816dacb 100644
--- a/src/clientfactory.ts
+++ b/src/clientfactory.ts
@@ -4,6 +4,8 @@ import { Client } from "discord.js";
 import * as log from "npmlog";
 import * as Bluebird from "bluebird";
 
+const READY_TIMEOUT = 5000;
+
 export class DiscordClientFactory {
   private config: DiscordBridgeConfigAuth;
   private store: DiscordStore;
@@ -23,10 +25,11 @@ export class DiscordClientFactory {
       sync: true,
       messageCacheLifetime: 5,
     }));
-    return this.botClient.login(this.config.botToken).then(() => {
-      return null; // Strip token from promise.
-    }).catch((err) => {
-      log.error("ClientFactory", "Could not login as the bot user. This is bad!");
+    this.botClient.login(this.config.botToken);
+    return this.botClient.onAsync("ready")
+    .timeout(READY_TIMEOUT, "Bot timed out waiting for ready.")
+    .catch((err) => {
+      log.error("ClientFactory", "Could not login as the bot user. This is bad!", err);
     });
   }
 
-- 
GitLab