Skip to content

Instantly share code, notes, and snippets.

@Zorono
Created March 2, 2021 20:44
Show Gist options
  • Save Zorono/5b92658a515c0fdd20a717cf682eaa95 to your computer and use it in GitHub Desktop.
Save Zorono/5b92658a515c0fdd20a717cf682eaa95 to your computer and use it in GitHub Desktop.
Associating an in-game playerid with a Discord account

Associating an in-game player with a Discord account

Why

  • 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:

  1. Discord user enters /mystats and bot replies with info about their money, score, etc, and whether they're currently connected or not.
  2. Player "John12" enters command '!dsay hello discord' and bot sends that message on a discord channel
  3. 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.

How

Current & Goal Situations

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.

Current situation - no interaction between Discord & Game

You'll notice that I highlighted the id fields of both account types - these are acting as the identifiers for their type.

Goal situation - can talk to each other via identifiers

To get there, we have to understand different things.

Storing and Accessing Data

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).

Our underlying tools

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. Our updated visualization

Necessary changes

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: Our updated visualization

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.

Linking itself

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:

  1. The process should be as little tedious as possible for players
  2. The process should be reliable - i.e it cannot confuse two different Discord accounts
  3. The process should be scalable - i.e it should be as effective when only one player is doing it vs a hundred.
  4. 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

Common mistakes

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:

  1. Login to discord
  2. Connect to SA-MP server
  3. On discord, enter some command like !samplink <my_online_game_id>
  4. 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!

Solution (logic) overview

Link request logic visualization Unlink request logic visualization

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).

Implementation overview

  • 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.

Conclusion

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment