Discourse Trust Levels for Discord


(Alkaline Thunder) #1

Hey there everyone! I’ve been loving the hell out of the Discourse chat integration plugin, but I started to think of other ways I could integrate Discourse with Discord. I think I found a pretty neat idea - bringing Trust Levels to Discord.

I’m writing a Discord bot that will allow a user to link their discourse user to their discord through the bot. Then, when the bot sees that the user has, say, trust level 2, it’ll give them a role on the server that the bot is running on.

I plan to have it configurable to work with more than just trust levels, i.e, if you get admin on discourse, you get it on discord too.

There are a few things I’m not totally certain on how I’m going to accomplish them, namely linking user accounts together (maybe there’s some sort of OAuth2 magic I can use?) but I think it’s a really neat idea and I’ve already gotten the bot to at least connect to both discord and discourse and allow me to see a list of commands in the bot and hot-reload them.

The bot is written in node.js and fully configurable through a single json file - and setting up trust level roles in Discord is just as easy as creating any other Discord role - it’s just that the bot needs to know what trust levels get what roles.

For those interested and who want to help me out with this, here’s a github repo: GitHub - alkalinethunder/discourse-tl-for-discord: Discord bot that brings Discourse trust levels to Discord.


Up-to-date Node.js module for Discourse API?
(Jeff Atwood) #2

This sounds great! Happy to put some community encouragement fund behind this effort cc @erlend_sh


(Stephen) #4

There’s already a discord plugin for oauth2 authentication, have you tried it?


(Alkaline Thunder) #5

That’d work great for giving discourse access to your discord account, but I need to give the discord bot access to both your discord account and your discourse account and basically link the two together.

That way, periodically, the bot will check discourse and say “What trust level does this user have?” Discourse will tell it, then the bot will say “OK, Discord, assign this role to this user on this server because they have this trust level.”

That sounds amazing! Would be nice to set up a demo instance of the bot that shows all the features it supports. So basically I’d set up a Discourse instance for discussion about the bot and host the demo bot on the same server as that instance, then I’d set up a Discord guild for the bot that’s linked to that aforementioned Discourse instance.


(Alkaline Thunder) #6

Milestone reached: The bot now lets you run discourse login and it’ll send you a message looking like this:

That link is VERY long, because it’s an SSO provider URL. I decided to have the bot use Discourse’s built-in SSO provider protocol to handle logins. So you run discourse login, it grabs your discord user ID (and the server’s ID), stores it in its database, generates a nonce, stores that in the database with your discord user ID, and then it DMs you a link to the forum where you can login.

When you log in, Discourse will theoretically let the bot know, where the bot will then verify the SSO payload that Discourse sent it, then promptly store some of your Discourse account info in the database along with your Discord user ID and it’ll remove the nonce from your account.

Man, do I need a url shortener though.


(Alkaline Thunder) #7

Final update for today:

  • SSO is fully working, the bot runs an http server (no ssl just yet, maybe someone more knowledgable with Node can help set up the bot so it supports that?). So you run discourse login on the discord server, bot DMs you an SSO link, you click it, Discourse sends the bot an SSO callback, the bot validates and authenticates it, then it grabs your Discourse user ID and stores it in the bot’s database and you get a nice “Success!” message in your browser.

  • Apparently, according to the discord bot dev community I’m in, this bot has one of the most structured codebase out of all the Node-based bots written by people in that server. Not sure how I accomplished that when my native language is C++.

  • The bot won’t let you log in again once you’ve already linked your account - eventually it’ll have a periodic check that DMs you if it is unable to find your Discourse account, and then it’ll let you log in again.

  • I’ve fully documented the botconfig.json file.

  • The command interpreter received a lot of bugfixes.

  • I’ve tested the crypto stuff in the SSO portion of the bot.

    • The nonce expires as soon as your account is linked, so any links with that nonce expire as well and lead to an HTTP 401 when the bot gets the payload.
    • For the same reason, refreshing the “Success” page causes an HTTP 401.

Fun facts for this update:

  • I wrote this bot in less than 24 hours while recovering from a nasty flu. Gatorade, Advil and coffee are this bot’s best friend - and mine as well, because all I have now is a sore throat.

  • The bot is so modular in its command system that I have the overwhelming desire to port cowsay to it despite that having NOTHING to do with Discourse. But it can be done! In less than 100 lines of code!


(Justin DiRose) #8

This is a really great idea!

Just so I’m clear, does it require Discourse being the SSO master?


(Alkaline Thunder) #9

Yes. It was the best way I could think of that didn’t involve forcing anyone to install an extra Discourse plugin. Instead, the bulk of the configuration is done in Discord and in the bot’s config file.

I haven’t tested it with my production Discourse instance, only my dev instance (https://webber-server.bitphoenixsoftware.com/), but it does in fact work there. I have my admin account over there linked up to my local instance of the bot.

My main concern is my production server redirects you to a Keycloak auth server to log in through openid connect. I haven’t tried Discourse’s SSO provider with that though, but I don’t see why it wouldn’t work. I’ll give it a try when the bot’s closer to a production state though.


(Stephen) #10

If you’re already authenticated and have a signed-in session how could it tell?


(Alkaline Thunder) #11

Depends what you mean - before you run the bot’s discourse login command or after you’ve already done that, clicked the link it DMs you, log in, etc.

Before you’ve run discourse login:

When you run the command, the bot grabs your user ID from the message’s author object. It checks that ID against a json file, which looks like this:

{
    "discord user id": {
        "sso_nonce": "",
        "sso_done": true,
        "discourse_uid": 0
    },

    "another discord user id": {
        "sso_nonce": "",
        "sso_done": false,
        "discourse_uid": 0
    }
}

So it’s basically a map of Discord user IDs to objects containing an SSO nonce, a value indicating whether SSO is done for that user (they’ve logged in before), and their Discourse user ID.

The bot checks if your Discourse user ID is in that file, and, if it is, then it checks if sso_done is true for that user. If so, it gives you an error saying “You’re already logged in.” If not, it generates and DMs you an SSO link, storing the nonce in that database so it can validate Discourse’s callback payload later.

SSO is only used to initially grant the bot access to your user account, without the bot needing to know your credentials.

Moreover, when you click that SSO link, if you’re already logged into Discourse, Discourse will just skip the login and send you straight to the bot so it can accept the login payload.

After you’ve initially logged in

NOTE: This is conceptual, I haven’t implemented this part yet.

From now on, as long as the bot has your Discord user ID on record and you’re still on the server, it can:

  1. check if sso_done is true before doing anything related to syncing your account.
  2. use the Discourse API to query info about your trust level, groups, title, etc.

Eventually I’ll add a logout command to the bot that lets you undo all syncing and remove your account from the bot’s database.

Feel free to look at the bot’s code, namely:

  1. Login command that generates the SSO link: discourse-tl-for-discord/login.command.js at master · alkalinethunder/discourse-tl-for-discord · GitHub

  2. SSO callback server - this is where Discourse’s callback payloads go: discourse-tl-for-discord/sso.js at master · alkalinethunder/discourse-tl-for-discord · GitHub


(Alkaline Thunder) #13

Milestone for today:

I’ve added a logout command, and a profile command. Doing discourse profile will show you your own user profile:

If you do discourse profile <username>, the bot will look for a user with a matching username and display its profile. This is the profile of my community admin on one of my Discourse instances:

It seems like I need to sanitize bio_excerpts since they contain raw HTML and Discord rich embeds don’t like that. They’ll parse links though, which is okay.

Leave it to him spamming his Twitch page to unknowingly find a bug in my code :slight_smile:

If the bot can’t find a user with a matching username, it’ll tell you:

It may even let you see discobot:

Actually, part of me wants to name this Discord bot ‘discobot’ because, although they don’t accomplish the same tasks, they’re both robots and related to Discourse. :slight_smile:


(Alkaline Thunder) #14

Another milestone for today:

The bot can now sync user titles and names.

The bot can be configured to prefer the user’s full name (default) or their username. In this case, my friend Trey has the user title “Community Admin” and the name “Anna Mey” - so his Discord nickname becomes that.

A small issue…

The bot can’t manage any user who has a higher role than the bot. It cannot manage the server’s owner either. This is a good thing, however it does mean that the bot cannot sync your account if you own the server.

To get around the fact the bot can’t sync users with a higher role, you eliminate all users with a higher role - a.k.a, make the bot’s role the highest role.


(Alkaline Thunder) #15

Got another big update for ya’s!

1. Unified user database

Yesterday, when the bot needed to see what users were logged in, it’d read from disk every time. SSD users will cringe when I say that. But, I’ve fixed that - the bot keeps the database resident in memory and saves it to disk when it changes. Far less disk operations, and it makes the bot slightly faster.

2. Trust levels are finally implemented!

That’s right! The main idea of the bot is actually working. I was able to get my friend Trey, who is currently my guinea pig, to have his trust level synced.

(Leader = trust level 4).

Every time the bot syncs a user, it:

  • Asks the Discourse API for the user’s trust level.
  • Looks up the trust level in the bot’s configuration to see what Discord role is assigned to it, if any.
  • If the Discord role it finds is valid, and the user doesn’t have it, the user is granted the role.
  • The bot then looks at all the other trust level roles in the bot’s config file.
  • If the user has one of those roles, and the relevant trust level doesn’t match the user’s current trust level, the user is removed from that role.

You’ll see this happening in your Discord guild’s audit log:


(Alkaline Thunder) #16

More role syncs done - admins and moderators are now synced.


(Rafael dos Santos Silva) #17

Awesome progress!

This plugin means users will have to deal with hosting it somewhere. Any reason you didn’t made it as a Discourse plugin so people could configure and run it alongside Discourse?


(Alkaline Thunder) #18

Thanks! :slight_smile:

The main reason is I’m far more familiar with JavaScript and Discord.js than I am with Ruby and writing Discourse plugins.

HOWEVER, it isn’t super difficult to run the bot on the same server as the Discourse instance and run the two behind an nginx proxy (in fact, that has a benefit since nginx will handle your SSL - the bot’s integrated http server doesn’t support SSL).

When the bot is ready to go out of testing, I’m going to write detailed wiki pages on the GitHub repo on how to configure the bot to run alongside Discourse on the same server.

Another reason it’s not a Discourse plugin is, if it weren’t for the integrated web server, the bot would be completely independent of Discourse other than API requests - the web server is only there because… your SSO callback payloads need to go somewhere!

Also, @erlend_sh gave me a feature request in PMs - support for custom groups. I have successfully added that. I used the built-in Staff group for testing.

I noticed that the Discourse API doesn’t treat the Staff group the same way as admins/moderators/trust levels. While all those aforementioned groups do end up in the user.groups list, those groups are also represented in their own fields in the user object. trust_level being an integer for your trust level, admin being a boolean, and moderator being a boolean. The staff group isn’t treated in that way - so it’s perfect for testing custom groups.


(Alkaline Thunder) #19

All the main features of the bot are tested and working in my local environment. I’ve also updated the readme and example configuration file for the bot to reflect the changes I’ve made. I’ll be focusing on documentation and the wiki, as well as public testing, for the next little while.


(Jeff Atwood) #21

Sorry, but we can’t financially support work that we can’t use. I hope this was made clear to you when you started. If you’re working on it for free on your own time, that’s fine of course!


(Alkaline Thunder) #22

Well, I have my own need for it anyway, I just don’t know Ruby as a language. As a concept, this thing is working perfectly fine - so if someone wants to help with porting to a Discourde plugin, I’m totally up for that - I’d just need to get up to speed with the language, how to use it, etc.

Actually, such a plugin would make installation far easier and would mean that configuration is solely done through the Discourse UI, moreover there’d be no need for an nginx reverse proxy or a json file or anything like that. So, porting it to a plugin might be a seriously good idea.

In fact, it can definitely be done by someone who actually knows the language. There’s a Discord API wrapper for Ruby: GitHub - meew0/discordrb: Discord API for Ruby


(Alkaline Thunder) #23

Hey there everyone - so, it’s official. I’m going to be learning Ruby and how to write discourse plugins. Also, soon this bot will be converted to one though it will not be done by me. I’m not sure who will do the initial porting but someone is already on it once they’ve finished another project. This will make installation and configuration much easier and will allow for some extra features.

I do know that once the initial porting is done, that will be what I play around with to learn. I may also try to implement some extra features into it that I haven’t gotten around to implementing in the Node bot, such as:

  • Requiring login to enter the Discord server (you need to be on the forum to be on the Discord, basically). This will be off by default.
  • Suspensions - I planned the feature but I haven’t yet implemented it because of the Ruby port being in the works, and me having to deal with school.
  • Staged and silenced users - not yet implemented for the same reason I haven’t yet implemented suspensions.
  • Settings within the bot and within your profile’s Preferences - so you can modify how the bot syncs certain things like your nickname.

There are some features I know I won’t implement because of the fact that the Discourse Chat Integrations plugin exists. That includes posting new Discourde topics to Discord, which would be possible if the bot is a Discourse plugin, but the Chat Integration plugin does this already so… why reinvent the wheel? :slight_smile: