Repository: evennia/evennia

Issue:
[BUG] Discord Integration - still creating 2 sessions every time it reconnects
#### Describe the bug
I have enabled discord integration with one channel in evennia to one channel in discord. It works well, but when it sits idle it manages to reconnect 2 sessions every time it disconnects. 

#### To Reproduce
Steps to reproduce the behavior:
1. follow the steps to create an evennia-discord integration
2.set up one channel in evennia to connect to one on a discord server
3. wait
4. See error
```
+--------------+--------+------+------------+--------+------+----------+------------+
| Account Name | On for | Idle | Puppeting  | Room   | Cmds | Protocol | Host       |
+--------------+--------+------+------------+--------+------+----------+------------+
| DiscordBot   | 00:19  | 19m  | None       | None   | 1    | discord  | discord.gg |
| DiscordBot   | 00:02  | 2m   | None       | None   | 1    | discord  | discord.gg |
| DiscordBot   | 00:02  | 2m   | None       | None   | 1    | discord  | discord.gg |
| Zephyr       | 02:28  | 4m   | Zephyr(#3) | RP Hub | 7    | telnet   | 127.0.0.1  |
+--------------+--------+------+------------+--------+------+----------+------------+
```
```
2 unique players logged in.
--------------------------------------------------------------------------------------
|Portal| 2022-12-07 15:51:16 [..] Portal starting server ... 
|Portal| 2022-12-07 15:51:17 [..] Connecting to Discord Gateway...
|Portal| 2022-12-07 15:51:17 [..] Attempting connection to Discord...
|Portal| 2022-12-07 17:08:05 [..] Discord disconnected: connection was closed uncleanly (peer dropped the TCP connection without previous WebSocket closing handshake)
|Portal| 2022-12-07 17:08:06 [..] Attempting connection to Discord...
|Portal| 2022-12-07 18:00:56 [..] Discord: received 'Reconnect' opcode. Reconnecting.
|Portal| 2022-12-07 18:00:56 [..] Attempting connection to Discord...
|Portal| 2022-12-07 18:00:56 [..] Discord disconnected: connection was closed uncleanly (peer dropped the TCP connection without previous WebSocket closing handshake)
|Portal| 2022-12-07 18:00:58 [..] Attempting connection to Discord...
|Portal| 2022-12-07 18:17:25 [..] Discord: received 'Reconnect' opcode. Reconnecting.
|Portal| 2022-12-07 18:17:25 [..] Attempting connection to Discord...
|Portal| 2022-12-07 18:17:25 [..] Discord disconnected: connection was closed uncleanly (peer dropped the TCP connection without previous WebSocket closing handshake)
|Portal| 2022-12-07 18:17:28 [..] Attempting connection to Discord...
```

#### Expected behavior
when the discord integration times out, it should disconnect and only reconnect once.

#### Environment, Evennia version, OS etc
```
Evennia version: 1.0.1 (rev fd0abb4520)
OS: posix
Python: 3.11.0rc1
Twisted: 22.10.0
Django: 4.1.3
```
#### Additional context
Is there maybe some timing issue? or did I mess something up?



Discussion:
I used discord/remove to remove the channel. With the discord bot up and no channels, this issue doesn't seem to happen.  I was wrong, it does still happen, but it seems far less frequent. I wonder if maybe it happens more often if there are more channels connected?

Sounds like an issue in the logic, where a new bot is generated not knowing the old one is still alive. There shouldn't be multiple bots in the who list either.
It's weird, I'm getting the opposite issue, discord disconnects and doesn't
reconnect until I reload the server. I'm on latest git pull on main branch

On Sat, 10 Dec 2022, 10:16 am Griatch, ***@***.***> wrote:

> Sounds like an issue in the logic, where a new bot is generated not
> knowing the old one is still alive. There shouldn't be multiple bots in the
> who list either.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/evennia/evennia/issues/3021#issuecomment-1344877709>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AB6GSH5RJ7LIVRBT2XVPEJ3WMO4WFANCNFSM6AAAAAASXOAQTA>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>

Yep, ignore my previous note - I hadn't restarted the portal.  Now I'm
getting this spammed.  There is still only 1 Discord bot on the who list:
[MudInfo] [2022-12-11(04:30)]: DiscordBot disconnected (Logged in from
elsewhere. Disconnecting.)
[MudInfo] [2022-12-11(04:30)]: DiscordBot connected
[MudInfo] [2022-12-11(04:31)]: DiscordBot disconnected (Logged in from
elsewhere. Disconnecting.)
[MudInfo] [2022-12-11(04:31)]: DiscordBot connected

On Sat, Dec 10, 2022 at 10:44 AM Arithorn ***@***.***> wrote:

> It's weird, I'm getting the opposite issue, discord disconnects and
> doesn't reconnect until I reload the server. I'm on latest git pull on main
> branch
>
> On Sat, 10 Dec 2022, 10:16 am Griatch, ***@***.***> wrote:
>
>> Sounds like an issue in the logic, where a new bot is generated not
>> knowing the old one is still alive. There shouldn't be multiple bots in the
>> who list either.
>>
>> —
>> Reply to this email directly, view it on GitHub
>> <https://github.com/evennia/evennia/issues/3021#issuecomment-1344877709>,
>> or unsubscribe
>> <https://github.com/notifications/unsubscribe-auth/AB6GSH5RJ7LIVRBT2XVPEJ3WMO4WFANCNFSM6AAAAAASXOAQTA>
>> .
>> You are receiving this because you are subscribed to this thread.Message
>> ID: ***@***.***>
>>
>


Resolving patch:
diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py
--- a/evennia/commands/default/comms.py
+++ b/evennia/commands/default/comms.py
@@ -1925,6 +1925,7 @@ class CmdDiscord2Chan(COMMAND_DEFAULT_CLASS):
         /delete  - alias to remove
         /guild   - toggle the Discord server tag on/off
         /channel - toggle the Evennia/Discord channel tags on/off
+        /start   - tell the bot to start, in case it lost its connection
 
     Example:
         discord2chan mydiscord = 555555555555555
@@ -1943,6 +1944,7 @@ class CmdDiscord2Chan(COMMAND_DEFAULT_CLASS):
         "guild",
         "list",
         "remove",
+        "start",
     )
     locks = "cmd:serversetting(DISCORD_ENABLED) and pperm(Developer)"
     help_category = "Comms"
@@ -1973,6 +1975,13 @@ def func(self):
                 f"WARNING: The Discord bot's typeclass is '{discord_bot.typeclass_path}'. This does not match {settings.DISCORD_BOT_CLASS} in settings!"
             )
 
+        if "start" in self.switches:
+            if discord_bot.sessions.all():
+                self.msg("The Discord bot is already running.")
+            else:
+                discord_bot.start()
+            return
+
         if "guild" in self.switches:
             discord_bot.db.tag_guild = not discord_bot.db.tag_guild
             self.msg(
diff --git a/evennia/server/portal/discord.py b/evennia/server/portal/discord.py
--- a/evennia/server/portal/discord.py
+++ b/evennia/server/portal/discord.py
@@ -84,7 +84,7 @@ def should_retry(status_code):
 
 class DiscordWebsocketServerFactory(WebSocketClientFactory, protocol.ReconnectingClientFactory):
     """
-    A variant of the websocket-factory that auto-reconnects.
+    A customized websocket client factory that navigates the Discord gateway process.
 
     """
 
@@ -94,7 +94,7 @@ class DiscordWebsocketServerFactory(WebSocketClientFactory, protocol.Reconnectin
     noisy = False
     gateway = None
     resume_url = None
-    do_retry = True
+    is_connecting = False
 
     def __init__(self, sessionhandler, *args, **kwargs):
         self.uid = kwargs.get("uid")
@@ -122,8 +122,8 @@ def cbResponse(response):
                 d = readBody(response)
                 d.addCallback(self.websocket_init, *args, **kwargs)
                 return d
-            elif should_retry(response.code):
-                delay(300, self.get_gateway_url, *args, **kwargs)
+            else:
+                logger.log_warn("Discord gateway request failed.")
 
         d.addCallback(cbResponse)
 
@@ -132,6 +132,7 @@ def websocket_init(self, payload, *args, **kwargs):
         callback for when the URL is gotten
         """
         data = json.loads(str(payload, "utf-8"))
+        self.is_connecting = False
         if url := data.get("url"):
             self.gateway = f"{url}/?v={DISCORD_API_VERSION}&encoding=json".encode("utf-8")
             useragent = kwargs.pop("useragent", DISCORD_USER_AGENT)
@@ -179,30 +180,7 @@ def startedConnecting(self, connector):
             connector (Connector): Represents the connection.
 
         """
-        logger.log_info("Attempting connection to Discord...")
-
-    def clientConnectionFailed(self, connector, reason):
-        """
-        Called when Client failed to connect.
-
-        Args:
-            connector (Connection): Represents the connection.
-            reason (str): The reason for the failure.
-
-        """
-        protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
-
-    def clientConnectionLost(self, connector, reason):
-        """
-        Called when Client loses connection.
-
-        Args:
-            connector (Connection): Represents the connection.
-            reason (str): The reason for the failure.
-
-        """
-        if self.do_retry and self.bot:
-            self.retry(connector)
+        logger.log_info("Connecting to Discord...")
 
     def reconnect(self):
         """
@@ -210,33 +188,30 @@ def reconnect(self):
         de-registering the session and then reattaching a new one.
 
         """
-        # set the retry flag to False so it doesn't attempt an automatic retry
-        # and duplicate the connection
-        self.do_retry = False
-        # disconnect everything
-        self.bot.transport.loseConnection()
-        self.sessionhandler.server_disconnect(self.bot)
         # set up the reconnection
         if self.resume_url:
             self.url = self.resume_url
         elif self.gateway:
             self.url = self.gateway
         else:
-            # we don't know where to reconnect to! start from the beginning
-            self.get_gateway_url()
-            return
-        self.start()
+            # we don't know where to reconnect to! we'll start from the beginning
+            self.url = None
+        # reset the internal delay, since this is a deliberate disconnect
+        self.delay = self.initialDelay
+        # disconnect to allow the reconnection process to kick in
+        self.bot.sendClose()
+        self.sessionhandler.server_disconnect(self.bot)
 
     def start(self):
         "Connect protocol to remote server"
 
         if not self.gateway:
-            # we can't actually start yet
+            # we don't know where to connect to
             # get the gateway URL from Discord
+            self.is_connecting = True
             self.get_gateway_url()
-        else:
-            # set the retry flag so we maintain this connection
-            self.do_retry = True
+        elif not self.is_connecting:
+            # everything is good, connect
             connectWS(self)
 
 
@@ -255,7 +230,6 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
     def __init__(self):
         WebSocketClientProtocol.__init__(self)
         _BASE_SESSION_CLASS.__init__(self)
-        self.restart_downtime = None
 
     def at_login(self):
         pass
@@ -265,8 +239,7 @@ def onOpen(self):
         Called when connection is established.
 
         """
-        self.restart_downtime = None
-        self.restart_task = None
+        logger.log_msg("Discord connection established.")
         self.factory.bot = self
 
         self.init_session("discord", "discord.gg", self.factory.sessionhandler)
@@ -352,11 +325,11 @@ def onClose(self, wasClean, code=None, reason=None):
         """
         if self.nextHeartbeatCall:
             self.nextHeartbeatCall.cancel()
-        self.disconnect(reason)
-        if code >= 4000:
-            logger.log_err(f"Discord connection closed: {reason}")
+            self.nextHeartbeatCall = None
+        if wasClean:
+            logger.log_info(f"Discord connection closed ({code}) reason: {reason}")
         else:
-            logger.log_info(f"Discord disconnected: {reason}")
+            logger.log_info(f"Discord connection lost.")
 
     def _send_json(self, data):
         """


Test patch: