From 4e7222ea7f17c0cbfddc852d5a9f1d8036164c34 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Sun, 27 Nov 2022 22:56:13 -0700 Subject: [PATCH] cleanup and comments --- docs/source/Setup/Channels-to-Discord.md | 26 ++++++++++----- evennia/commands/default/comms.py | 4 +-- evennia/server/portal/discord.py | 40 +++++++++++++++--------- evennia/settings_default.py | 16 ++++++++-- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/docs/source/Setup/Channels-to-Discord.md b/docs/source/Setup/Channels-to-Discord.md index 4bae6ff08f..87ff92aec3 100644 --- a/docs/source/Setup/Channels-to-Discord.md +++ b/docs/source/Setup/Channels-to-Discord.md @@ -7,12 +7,12 @@ to your in-game channels to communicate between in-game and out. ## Configuring Discord The first thing you'll need is to set up a Discord bot to connect to your game. -Go to the [bot applications](https://discord.com/developers/applications) page page and make a new application. You'll need the +Go to the [bot applications](https://discord.com/developers/applications) page and make a new application. You'll need the "MESSAGE CONTENT" toggle flipped On, and to add your bot token to your settings. ```python # mygame/server/conf/secret_settings.py -DISCORD_BOT_TOKEN = +DISCORD_BOT_TOKEN = '' ``` You will also need the `pyopenssl` module, if it isn't already installed. @@ -37,9 +37,9 @@ Adding a new channel link is done with the following command: The `evennia_channel` argument must be the name of an existing Evennia channel, and `discord_channel_id` is the full numeric ID of the Discord channel. -> Your bot needs to be added to the correct server with access to the channel -> in order to send or receive messages. This command does NOT verify that your -> bot has access! +> Your bot needs to be added to the correct Discord server with access to the +> channel in order to send or receive messages. This command does NOT verify that +> your bot has Discord permissions! ## Step-By-Step Discord Setup @@ -53,7 +53,7 @@ steps already, feel free to skip to the next. > in order to connect Evennia to it. This assumes you already do. Make sure you're logged in on the Discord website, then visit -[https://discord.com/developers/applications]. Click the "New Application" +https://discord.com/developers/applications. Click the "New Application" button in the upper right corner, then enter the name for your new app - the name of your Evennia game is a good option. @@ -69,7 +69,7 @@ Next, add this token to your _secret_ settings. ```python # file: mygame/server/conf/secret_settings.py -DISCORD_BOT_TOKEN = +DISCORD_BOT_TOKEN = '' ``` Once that is saved, scroll down the Bot page a little more and find the toggle for @@ -166,4 +166,14 @@ DISCORD_BOT_CLASS = 'accounts.bots.DiscordBot' > If you had already set up a Discord relay and are changing this, make sure you > either delete the old bot account in Evennia or change its typeclass or it won't -> take effect. \ No newline at end of file +> take effect. + +The core DiscordBot account class has several useful hooks already set up for +processing and relaying channel messages between Discord and Evennia channels, +along with the (unused by default) `direct_msg` hook for processing DMs sent to +the bot on Discord. + +Only messages and server updates are processed by default, but the Discord custom +protocol passes all other unprocessed dispatch data on to the Evennia bot account +so you can add additional handling yourself. However, **this integration is not a full library** +and does not document the full range of possible Discord events. \ No newline at end of file diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 37c373afe1..df848c86eb 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -1964,8 +1964,8 @@ class CmdDiscord2Chan(COMMAND_DEFAULT_CLASS): if not discord_bot: if "name" in self.switches: # create a new discord bot - # TODO: reference settings for custom typeclass - discord_bot = create.create_account(self.lhs, None, None, typeclass=bots.DiscordBot) + bot_class = class_from_module(settings.DISCORD_BOT_CLASS, fallback=bots.DiscordBot) + discord_bot = create.create_account(self.lhs, None, None, typeclass=bot_class) discord_bot.start() else: self.msg("Please set up your Discord bot first: discord2chan/name ") diff --git a/evennia/server/portal/discord.py b/evennia/server/portal/discord.py index 4f900b9062..e0b8b3bc53 100644 --- a/evennia/server/portal/discord.py +++ b/evennia/server/portal/discord.py @@ -35,8 +35,8 @@ DISCORD_API_VERSION = 10 DISCORD_API_BASE_URL = f"https://discord.com/api/v{DISCORD_API_VERSION}" DISCORD_USER_AGENT = f"Evennia (https://www.evennia.com, {get_evennia_version(mode='short')})" -DISCORD_BOT_TOKEN = getattr(settings, "DISCORD_BOT_TOKEN", None) -DISCORD_BOT_INTENTS = getattr(settings, "DISCORD_BOT_INTENTS", 105985) +DISCORD_BOT_TOKEN = settings.DISCORD_BOT_TOKEN +DISCORD_BOT_INTENTS = settings.DISCORD_BOT_INTENTS # Discord OP codes, alphabetic OP_DISPATCH = 0 @@ -83,7 +83,7 @@ class DiscordWebsocketServerFactory(WebSocketClientFactory, protocol.Reconnectin ) def cbResponse(response): - # check status code here to verify it was a successful connection first + # TODO: check status code here to verify it was a successful connection first # then schedule a retry if not d = readBody(response) d.addCallback(self.websocket_init, *args, **kwargs) @@ -176,7 +176,6 @@ class DiscordWebsocketServerFactory(WebSocketClientFactory, protocol.Reconnectin de-registering the session and then reattaching a new one. """ - self.bot.stopping = True self.bot.transport.loseConnection() self.sessionhandler.server_disconnect(self.bot) if self.resume_url: @@ -228,8 +227,6 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): """ self.restart_downtime = None self.restart_task = None - - self.stopping = False self.factory.bot = self self.init_session("discord", "discord.gg", self.factory.sessionhandler) @@ -275,11 +272,13 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): else: self.identify() elif data["op"] == OP_HEARTBEAT_ACK: + # our last heartbeat was acknowledged, so reset the "pending" flag self.pending_heartbeat = False elif data["op"] == OP_HEARTBEAT: + # Discord wants us to send a heartbeat immediately self.doHeartbeat(force=True) elif data["op"] == OP_INVALID_SESSION: - # reconnect + # Discord doesn't like our current session; reconnect for a new one logger.log_msg("Discord: received 'Invalid Session' opcode. Reconnecting.") if data["d"] == False: # can't resume, clear existing resume data @@ -287,10 +286,13 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): self.factory.resume_url = None self.factory.reconnect() elif data["op"] == OP_RECONNECT: + # reconnect as requested; Discord does this regularly for server load balancing logger.log_msg("Discord: received 'Reconnect' opcode. Reconnecting.") self.factory.reconnect() elif data["op"] == OP_DISPATCH: + # handle the general dispatch opcode events by type if data["t"] == "READY": + # our recent identification is valid; process new session info self.connection_ready(data["d"]) else: # general message, pass on to data_in @@ -331,7 +333,7 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): Post JSON data to a REST API endpoint Args: - url (str) - + url (str) - The API path which is being posted to data (dict) - Content to be sent """ url = f"{DISCORD_API_BASE_URL}/{url}" @@ -350,7 +352,7 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): ) def cbResponse(response): - # check status code here to verify it was a successful connection first + # TODO: check status code here to verify it was a successful connection first # then schedule a retry if not d = readBody(response) d.addCallback(self.post_response) @@ -388,6 +390,7 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): # we have no known state to resume from, identify normally self.identify() + # build a RESUME request for Discord and send it data = { "op": OP_RESUME, "d": { @@ -445,8 +448,10 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): if not self.pending_heartbeat or kwargs.get("force"): if self.nextHeartbeatCall: self.nextHeartbeatCall.cancel() + # send the heartbeat data = {"op": 1, "d": self.last_sequence} self._send_json(data) + # track that we sent a heartbeat, in case we don't receive an ACK self.pending_heartbeat = True self.nextHeartbeatCall = self.factory._batched_timer.call_later( self.interval, @@ -477,23 +482,25 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): def data_in(self, data, **kwargs): """ + Process incoming data from Discord and sent to the Evennia server - Send data grapevine -> Evennia - Keyword Args: + Args: data (dict): Converted json data. """ action_type = data.get("t", "UNKNOWN") if action_type == "MESSAGE_CREATE": + # someone posted a message on Discord that the bot can see data = data["d"] if data["author"]["id"] == self.discord_id: + # it's by the bot itself! disregard return message = data["content"] channel_id = data["channel_id"] keywords = {"channel_id": channel_id} if "guild_id" in data: - # channel message + # message received to a Discord channel keywords["type"] = "channel" author = data["member"]["nick"] or data["author"]["username"] author_id = data["author"]["id"] @@ -501,15 +508,17 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): keywords["guild_id"] = data["guild_id"] else: - # direct message + # message sent directly to the bot account via DM keywords["type"] = "direct" author = data["author"]["username"] author_id = data["author"]["id"] keywords["sender"] = (author_id, author) + # pass the processed data to the server self.sessionhandler.data_in(self, bot_data_in=(message, keywords)) elif action_type in ("GUILD_CREATE", "GUILD_UPDATE"): + # we received the current status of a guild the bot is on; process relevant info data = data["d"] keywords = {"type": "guild", "guild_id": data["id"], "guild_name": data["name"]} keywords["channels"] = { @@ -517,15 +526,16 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS): for chan in data["channels"] if chan["type"] == 0 } + # send the possibly-updated guild and channel data to the server self.sessionhandler.data_in(self, bot_data_in=("", keywords)) elif "DELETE" in action_type: - # deletes should probably be handled separately to check for channel removal + # deletes should possibly be handled separately to check for channel removal # for now, just ignore pass else: - # send all the data on to the bot as-is for optional bot-side handling + # send the data for any other action types on to the bot as-is for optional server-side handling keywords = {"type": action_type} keywords.update(data["d"]) self.sessionhandler.data_in(self, bot_data_in=("", keywords)) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 6bb8b255ce..38f372255f 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -874,9 +874,21 @@ GRAPEVINE_CHANNELS = ["gossip", "testing"] # them. These are secret and should thus be overridden in secret_settings file GRAPEVINE_CLIENT_ID = "" GRAPEVINE_CLIENT_SECRET = "" -# Discord integration -# TODO: add doc comments here +# Discord (discord.com) is a popular communication service for many, especially +# for game communities. Evennia's channels can be connected to Discord channels +# and relay messages between Evennia and Discord. To use, you will need to create +# your own Discord application and bot. +# Discord also requires installing the pyopenssl library. +# Full step-by-step instructions are available in the official Evennia documentation. DISCORD_ENABLED = False +# The Intents bitmask required by Discord bots to request particular API permissions. +# By default, this includes the basic guild status and message read/write flags. +DISCORD_BOT_INTENTS = 105985 +# The authentication token for the Discord bot. This should be kept secret and +# put in your secret_settings file. +DISCORD_BOT_TOKEN = None +# The account typeclass which the Evennia-side Discord relay bot will use. +DISCORD_BOT_CLASS = "evennia.accounts.bots.DiscordBot" ###################################################################### # Django web features