cleanup and comments
This commit is contained in:
parent
a2eb049fc9
commit
4e7222ea7f
4 changed files with 59 additions and 27 deletions
|
|
@ -7,12 +7,12 @@ to your in-game channels to communicate between in-game and out.
|
||||||
## Configuring Discord
|
## Configuring Discord
|
||||||
|
|
||||||
The first thing you'll need is to set up a Discord bot to connect to your game.
|
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.
|
"MESSAGE CONTENT" toggle flipped On, and to add your bot token to your settings.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# mygame/server/conf/secret_settings.py
|
# mygame/server/conf/secret_settings.py
|
||||||
DISCORD_BOT_TOKEN = <your Discord bot token>
|
DISCORD_BOT_TOKEN = '<your Discord bot token>'
|
||||||
```
|
```
|
||||||
|
|
||||||
You will also need the `pyopenssl` module, if it isn't already installed.
|
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,
|
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.
|
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
|
> Your bot needs to be added to the correct Discord server with access to the
|
||||||
> in order to send or receive messages. This command does NOT verify that your
|
> channel in order to send or receive messages. This command does NOT verify that
|
||||||
> bot has access!
|
> your bot has Discord permissions!
|
||||||
|
|
||||||
## Step-By-Step Discord Setup
|
## 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.
|
> in order to connect Evennia to it. This assumes you already do.
|
||||||
|
|
||||||
Make sure you're logged in on the Discord website, then visit
|
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
|
button in the upper right corner, then enter the name for your new app - the
|
||||||
name of your Evennia game is a good option.
|
name of your Evennia game is a good option.
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ Next, add this token to your _secret_ settings.
|
||||||
```python
|
```python
|
||||||
# file: mygame/server/conf/secret_settings.py
|
# file: mygame/server/conf/secret_settings.py
|
||||||
|
|
||||||
DISCORD_BOT_TOKEN = <token>
|
DISCORD_BOT_TOKEN = '<token>'
|
||||||
```
|
```
|
||||||
|
|
||||||
Once that is saved, scroll down the Bot page a little more and find the toggle for
|
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
|
> 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
|
> either delete the old bot account in Evennia or change its typeclass or it won't
|
||||||
> take effect.
|
> 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.
|
||||||
|
|
@ -1964,8 +1964,8 @@ class CmdDiscord2Chan(COMMAND_DEFAULT_CLASS):
|
||||||
if not discord_bot:
|
if not discord_bot:
|
||||||
if "name" in self.switches:
|
if "name" in self.switches:
|
||||||
# create a new discord bot
|
# create a new discord bot
|
||||||
# TODO: reference settings for custom typeclass
|
bot_class = class_from_module(settings.DISCORD_BOT_CLASS, fallback=bots.DiscordBot)
|
||||||
discord_bot = create.create_account(self.lhs, None, None, typeclass=bots.DiscordBot)
|
discord_bot = create.create_account(self.lhs, None, None, typeclass=bot_class)
|
||||||
discord_bot.start()
|
discord_bot.start()
|
||||||
else:
|
else:
|
||||||
self.msg("Please set up your Discord bot first: discord2chan/name <bot_name>")
|
self.msg("Please set up your Discord bot first: discord2chan/name <bot_name>")
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ DISCORD_API_VERSION = 10
|
||||||
DISCORD_API_BASE_URL = f"https://discord.com/api/v{DISCORD_API_VERSION}"
|
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_USER_AGENT = f"Evennia (https://www.evennia.com, {get_evennia_version(mode='short')})"
|
||||||
DISCORD_BOT_TOKEN = getattr(settings, "DISCORD_BOT_TOKEN", None)
|
DISCORD_BOT_TOKEN = settings.DISCORD_BOT_TOKEN
|
||||||
DISCORD_BOT_INTENTS = getattr(settings, "DISCORD_BOT_INTENTS", 105985)
|
DISCORD_BOT_INTENTS = settings.DISCORD_BOT_INTENTS
|
||||||
|
|
||||||
# Discord OP codes, alphabetic
|
# Discord OP codes, alphabetic
|
||||||
OP_DISPATCH = 0
|
OP_DISPATCH = 0
|
||||||
|
|
@ -83,7 +83,7 @@ class DiscordWebsocketServerFactory(WebSocketClientFactory, protocol.Reconnectin
|
||||||
)
|
)
|
||||||
|
|
||||||
def cbResponse(response):
|
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
|
# then schedule a retry if not
|
||||||
d = readBody(response)
|
d = readBody(response)
|
||||||
d.addCallback(self.websocket_init, *args, **kwargs)
|
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.
|
de-registering the session and then reattaching a new one.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.bot.stopping = True
|
|
||||||
self.bot.transport.loseConnection()
|
self.bot.transport.loseConnection()
|
||||||
self.sessionhandler.server_disconnect(self.bot)
|
self.sessionhandler.server_disconnect(self.bot)
|
||||||
if self.resume_url:
|
if self.resume_url:
|
||||||
|
|
@ -228,8 +227,6 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
"""
|
"""
|
||||||
self.restart_downtime = None
|
self.restart_downtime = None
|
||||||
self.restart_task = None
|
self.restart_task = None
|
||||||
|
|
||||||
self.stopping = False
|
|
||||||
self.factory.bot = self
|
self.factory.bot = self
|
||||||
|
|
||||||
self.init_session("discord", "discord.gg", self.factory.sessionhandler)
|
self.init_session("discord", "discord.gg", self.factory.sessionhandler)
|
||||||
|
|
@ -275,11 +272,13 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
else:
|
else:
|
||||||
self.identify()
|
self.identify()
|
||||||
elif data["op"] == OP_HEARTBEAT_ACK:
|
elif data["op"] == OP_HEARTBEAT_ACK:
|
||||||
|
# our last heartbeat was acknowledged, so reset the "pending" flag
|
||||||
self.pending_heartbeat = False
|
self.pending_heartbeat = False
|
||||||
elif data["op"] == OP_HEARTBEAT:
|
elif data["op"] == OP_HEARTBEAT:
|
||||||
|
# Discord wants us to send a heartbeat immediately
|
||||||
self.doHeartbeat(force=True)
|
self.doHeartbeat(force=True)
|
||||||
elif data["op"] == OP_INVALID_SESSION:
|
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.")
|
logger.log_msg("Discord: received 'Invalid Session' opcode. Reconnecting.")
|
||||||
if data["d"] == False:
|
if data["d"] == False:
|
||||||
# can't resume, clear existing resume data
|
# can't resume, clear existing resume data
|
||||||
|
|
@ -287,10 +286,13 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
self.factory.resume_url = None
|
self.factory.resume_url = None
|
||||||
self.factory.reconnect()
|
self.factory.reconnect()
|
||||||
elif data["op"] == OP_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.")
|
logger.log_msg("Discord: received 'Reconnect' opcode. Reconnecting.")
|
||||||
self.factory.reconnect()
|
self.factory.reconnect()
|
||||||
elif data["op"] == OP_DISPATCH:
|
elif data["op"] == OP_DISPATCH:
|
||||||
|
# handle the general dispatch opcode events by type
|
||||||
if data["t"] == "READY":
|
if data["t"] == "READY":
|
||||||
|
# our recent identification is valid; process new session info
|
||||||
self.connection_ready(data["d"])
|
self.connection_ready(data["d"])
|
||||||
else:
|
else:
|
||||||
# general message, pass on to data_in
|
# 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
|
Post JSON data to a REST API endpoint
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url (str) -
|
url (str) - The API path which is being posted to
|
||||||
data (dict) - Content to be sent
|
data (dict) - Content to be sent
|
||||||
"""
|
"""
|
||||||
url = f"{DISCORD_API_BASE_URL}/{url}"
|
url = f"{DISCORD_API_BASE_URL}/{url}"
|
||||||
|
|
@ -350,7 +352,7 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
)
|
)
|
||||||
|
|
||||||
def cbResponse(response):
|
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
|
# then schedule a retry if not
|
||||||
d = readBody(response)
|
d = readBody(response)
|
||||||
d.addCallback(self.post_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
|
# we have no known state to resume from, identify normally
|
||||||
self.identify()
|
self.identify()
|
||||||
|
|
||||||
|
# build a RESUME request for Discord and send it
|
||||||
data = {
|
data = {
|
||||||
"op": OP_RESUME,
|
"op": OP_RESUME,
|
||||||
"d": {
|
"d": {
|
||||||
|
|
@ -445,8 +448,10 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
if not self.pending_heartbeat or kwargs.get("force"):
|
if not self.pending_heartbeat or kwargs.get("force"):
|
||||||
if self.nextHeartbeatCall:
|
if self.nextHeartbeatCall:
|
||||||
self.nextHeartbeatCall.cancel()
|
self.nextHeartbeatCall.cancel()
|
||||||
|
# send the heartbeat
|
||||||
data = {"op": 1, "d": self.last_sequence}
|
data = {"op": 1, "d": self.last_sequence}
|
||||||
self._send_json(data)
|
self._send_json(data)
|
||||||
|
# track that we sent a heartbeat, in case we don't receive an ACK
|
||||||
self.pending_heartbeat = True
|
self.pending_heartbeat = True
|
||||||
self.nextHeartbeatCall = self.factory._batched_timer.call_later(
|
self.nextHeartbeatCall = self.factory._batched_timer.call_later(
|
||||||
self.interval,
|
self.interval,
|
||||||
|
|
@ -477,23 +482,25 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
|
|
||||||
def data_in(self, data, **kwargs):
|
def data_in(self, data, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
Process incoming data from Discord and sent to the Evennia server
|
||||||
|
|
||||||
Send data grapevine -> Evennia
|
Args:
|
||||||
Keyword Args:
|
|
||||||
data (dict): Converted json data.
|
data (dict): Converted json data.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
action_type = data.get("t", "UNKNOWN")
|
action_type = data.get("t", "UNKNOWN")
|
||||||
|
|
||||||
if action_type == "MESSAGE_CREATE":
|
if action_type == "MESSAGE_CREATE":
|
||||||
|
# someone posted a message on Discord that the bot can see
|
||||||
data = data["d"]
|
data = data["d"]
|
||||||
if data["author"]["id"] == self.discord_id:
|
if data["author"]["id"] == self.discord_id:
|
||||||
|
# it's by the bot itself! disregard
|
||||||
return
|
return
|
||||||
message = data["content"]
|
message = data["content"]
|
||||||
channel_id = data["channel_id"]
|
channel_id = data["channel_id"]
|
||||||
keywords = {"channel_id": channel_id}
|
keywords = {"channel_id": channel_id}
|
||||||
if "guild_id" in data:
|
if "guild_id" in data:
|
||||||
# channel message
|
# message received to a Discord channel
|
||||||
keywords["type"] = "channel"
|
keywords["type"] = "channel"
|
||||||
author = data["member"]["nick"] or data["author"]["username"]
|
author = data["member"]["nick"] or data["author"]["username"]
|
||||||
author_id = data["author"]["id"]
|
author_id = data["author"]["id"]
|
||||||
|
|
@ -501,15 +508,17 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
keywords["guild_id"] = data["guild_id"]
|
keywords["guild_id"] = data["guild_id"]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# direct message
|
# message sent directly to the bot account via DM
|
||||||
keywords["type"] = "direct"
|
keywords["type"] = "direct"
|
||||||
author = data["author"]["username"]
|
author = data["author"]["username"]
|
||||||
author_id = data["author"]["id"]
|
author_id = data["author"]["id"]
|
||||||
keywords["sender"] = (author_id, author)
|
keywords["sender"] = (author_id, author)
|
||||||
|
|
||||||
|
# pass the processed data to the server
|
||||||
self.sessionhandler.data_in(self, bot_data_in=(message, keywords))
|
self.sessionhandler.data_in(self, bot_data_in=(message, keywords))
|
||||||
|
|
||||||
elif action_type in ("GUILD_CREATE", "GUILD_UPDATE"):
|
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"]
|
data = data["d"]
|
||||||
keywords = {"type": "guild", "guild_id": data["id"], "guild_name": data["name"]}
|
keywords = {"type": "guild", "guild_id": data["id"], "guild_name": data["name"]}
|
||||||
keywords["channels"] = {
|
keywords["channels"] = {
|
||||||
|
|
@ -517,15 +526,16 @@ class DiscordClient(WebSocketClientProtocol, _BASE_SESSION_CLASS):
|
||||||
for chan in data["channels"]
|
for chan in data["channels"]
|
||||||
if chan["type"] == 0
|
if chan["type"] == 0
|
||||||
}
|
}
|
||||||
|
# send the possibly-updated guild and channel data to the server
|
||||||
self.sessionhandler.data_in(self, bot_data_in=("", keywords))
|
self.sessionhandler.data_in(self, bot_data_in=("", keywords))
|
||||||
|
|
||||||
elif "DELETE" in action_type:
|
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
|
# for now, just ignore
|
||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
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 = {"type": action_type}
|
||||||
keywords.update(data["d"])
|
keywords.update(data["d"])
|
||||||
self.sessionhandler.data_in(self, bot_data_in=("", keywords))
|
self.sessionhandler.data_in(self, bot_data_in=("", keywords))
|
||||||
|
|
|
||||||
|
|
@ -874,9 +874,21 @@ GRAPEVINE_CHANNELS = ["gossip", "testing"]
|
||||||
# them. These are secret and should thus be overridden in secret_settings file
|
# them. These are secret and should thus be overridden in secret_settings file
|
||||||
GRAPEVINE_CLIENT_ID = ""
|
GRAPEVINE_CLIENT_ID = ""
|
||||||
GRAPEVINE_CLIENT_SECRET = ""
|
GRAPEVINE_CLIENT_SECRET = ""
|
||||||
# Discord integration
|
# Discord (discord.com) is a popular communication service for many, especially
|
||||||
# TODO: add doc comments here
|
# 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
|
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
|
# Django web features
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue