- Want to have a Discord user be able to enter commands to a bot and have actions impact their account in-game or not.
- Want an in-game user to be able to enter commands and have a bot "carry those over" to Discord
- Want to have the bot be able to access players' information
Examples:
- Discord user enters /mystats and bot replies with info about their money, score, etc, and whether they're currently connected or not.
- Player "John12" enters command '!dsay hello discord' and bot sends that message on a discord channel
- Discord user enters /topstats and the discord bot can get the necessary information and reply with it on discord
Note: these are only specific examples to motivate the method of associating game accounts with Discord accounts.
For different things, we come up with ways of uniquely addressing them, or "identifiers" - these help us be able to tell one thing from another. Here it is important to understand that there are essentially two "systems" that currently don't "talk" with each other but we'd like them to.
You'll notice that I highlighted the id
fields of both account types - these are acting as the identifiers for their type.
To get there, we have to understand different things.
Data can be anything - names, addresses, money, etc. Most applications use data in one way or another to achieve their functionality. Let's think about your SA-MP server. Warning: simplifications are used below - keep the bigger picture in mind.
Initially, you start off by making scripts and realize that hardcoding information is worse than separating it to variables. Once you get that to work, you realize that after you reconnect after a new server start, your progress is gone and you need to start over from zero. Here you run into the need of having a persistent storage medium. All that basically means is that you want to have some abstraction of a place where the data will not reset itself after a shutdown/reboot/etc, yet lets you read/write to it as you wish. Many of you may have started with options like .ini
files, but may have eventually migrated to MySQL
. You should investigate this yourself, but the main reason why it can be a more convenient alternative to .ini
files is that it is better at giving you access to operations that rely on the relationships between different data (tables). So, this guide will hereby assume that you are using MySQL
as an option.
Likewise, I'm assuming that you have a Discord bot already set up and that you have some means of letting it connect to some MySQL
database (your server's one).
I'm assuming you're using the samp-discord-connector plugin as your "bridge" connecting Discord and your server. You are thereby "limited" to what it offers: check the wiki for more information. That will pretty much decide what "Discord information" we can work with. Three natives that I will come back later are DCC_FindUserByName
, DCC_FindUserById
and DCC_GetUserId
.
As the wiki says
"Discord ID" is the actual ID that discord uses, whereas "Internal ID" is the integer needed to reference a given channel to the plugin. This is explained on the main page of this wiki.
So we can adjust our visualization to account for these and the MySQL assumptions.
At this point it should be evident that we need to update our Accounts
table schema. Why? First, read the wiki and get a feel for how the plugin is used. After this, you should be able to see that what the internal id (integer) is used for the majority of the functionality exposed by it, both in natives and callbacks. However, it is important to realize that we should not store the internal id itself! The internal id is very likely to change across server reboots: today your internal id might have been 3, but tomorrow it's 15 - we don't want to have these mistakes happening, remember what we said above about identifiers needing to be unique and correct all the time. So what do we do then?
We want to store the unique Discord ID that will never change for the user, not the internal value the plugin uses. Upon a player's login, we will be able to use that Discord id to get the internal id and subsequently use that for later calls to other functionality that the plugin provides. It will be convenient to store that internal id for a player's session as it will not change, which will help us avoid the unnecessary conversion from Discord id -> plugin id later down the line.
So, let's see how the updated visualization looks like:
We can see we added a discordID
field to our Accounts
table, which will match the Discord id for that Discord account. Note: Accounts.id
stays red to make obvious that these are different ids! One is used for Discord interaction via the plugin, the other is the internal id for the server account.
At this point we have an understanding of how the system will work once this link exist (i.e once Account.discordID
is correctly stored) but we have not discussed on the linking process itself. Some things to keep in mind are:
- The process should be as little tedious as possible for players
- The process should be reliable - i.e it cannot confuse two different Discord accounts
- The process should be scalable - i.e it should be as effective when only one player is doing it vs a hundred.
- The process should be versatile/error prone - i.e it should be able to deal with user mistakes and allow for corrections easily.
To meet demands 2 3 4, the following are necessary:
- Do not trust user input, and minimize the room for it whenever possible
- Have a small time window where a link request can be accepted, else it should be automatically declined and discarded
- Use some form of a (unique) "token" to verify requests which should expire after the time window
- Allow for the possibility to unlink a Discord account after it's been successfully linked
- Allow for the possibility to link a Discord account after it/another has been successfully unlinked, or no previous linked account
To meet demand 1, the following are necessary:
- Pleasant & intuitive interface
- Intuitive user interaction with clear instructions
- Minimize admin/moderator intervention - should ideally be a player-only process
A common misconception would be that the token itself needs to be stored in our schema. Don't fall for this! The token's only job is to validate a link/unlink operation, and as noted above it will have a very short lifespan.
Another mistake would be to have the process be:
- Login to discord
- Connect to SA-MP server
- On discord, enter some command like
!samplink <my_online_game_id>
- Show a dialog to player with that id, asking to confirm/deny.
The main mistake here, as well as not sanitizing the id to make sure that there is a player connected with it, is that usage of a dialog. A dialog will interrupt the gameplay experience, and it could be use by players to "troll" others and interrupt their gameplay when they themselves did not send the request!
You'll see that they're both very similar, and that's not by accident. The processes themselves differ only in what they're doing at the very end, and they fall under the same "Discord-SAMP operation" abstraction. Now I'll briefly describe some key aspects of this solution and then offer a proof that our formal demands are met.
- We use Discord with 2FA as the "catalyst"/source to both operations as it poses more hurdles to people trying to fuck about with the system than SA-MP.
- We use a key-value store to store the valid keys/tokens that can be used at a given time: some implementations offer built-in TTL capabilities which will invalidate entries after time is up.
- We have a volatile intendedToken variable that users will need to configure before confirming the link/unlink that helps with validating these actions
Proof:
- We're minimizing user input wherever possible, and id sanitizing is implicitly done by storing that info in the valid key object and used later in business logic
- We have a small time window (10min) after which any request will be invalid
- Our choice of key-value store provider will take care of the TTL for us
- Unlink capabilities work alongside token validation if an existing Discord account was already linked
- Link capabilities work alongside token validation if no existing Discord account was already linked/if an existing one was successfully unlinked
To satisfy demand 1:
- descriptive error messages can be used
- a different textdraw format (discrete, not overdone) can serve as the "notification hub" for link/unlink messages
- there is no need for admin/mod intervention (and there is also the possibility of manual fixing of
Account.discordId
if necessary i.e in the case of bans, etc).
- Use redis as the key-value store, and have it available both to discord bot and SA-MP (discord connector plugin)
- Use "iterator over array" approach for the intendedTokens for MAX_PLAYERS of MAX_TOKEN_LENGTH
- Use something of your choice to generate these random strings and minimize collisions (I'd personally use pawn-uuid)
- When a player disconnects, remove all tokens associated with that id as they would be invalid upon disconnect. The same applies for when the server goes offline.
As you can see I chose to focus much more on the motivation of the problem, understanding of the problem, devising a solution, and proving that solution that the actual implementation. No plan survives battle, so you may be in the position where you will apply different fixes/improvements, but that doesn't mean you go on without a plan. This is a good demonstration of what I cover on CAHOA here - it should prove that I practice what I preach.
Hope that you found this tutorial/discussion useful, if there's any questions you have don't be afraid to ask but remember to know how to ask a good question. I have no problem with spending "extra time" (as this post itself shows) but effort is a two way street - ask a lazy question, get a lazy answer, and viceversa.