Discord Presence

Before you proceed to make your own version of this project, I'd like to inform you that there already is a well-established foss and community for this called lanyard.


What I've essentially did here is:

  1. Create a bot and put it in a private guild
  2. Read my user's presence
  3. Feed it to the client through websockets

Creating the bot

Head over to the developer portal and create a new application. Make a new bot, with presence intent enabled as here:

Presence intent

Then put it in a private server your account is in. You can filter presences of course, but it's cleaner to just keep it in a channel that only your account is in.

Reading your presence

Now for this part, I've used serenity.rs.

Made a client and event handler with presence intent, and every presence update, created a simple json payload for the stuff I need. Which in serenity would look something like:

#[async_trait]
impl EventHandler for Handler {
    async fn ready(&self, _: Context, ready: Ready) {
        println!("{} is connected!", ready.user.name);
    }
    
    async fn presence_update(&self, _ctx: Context, new_presence: Presence) {
        println!("Presence Update");

        if new_presence.user.id.get() != PRESENCE_TRACKED_ID {
            return;
        }

        let mut payload = json!({
            "status": new_presence.status.name(),
            "activities": new_presence.activities
        });

        for activity in new_presence.activities {
            println!("{}", activity.name.to_string());
            let mut activity_payload = json!({
                "name": activity.name.to_string()
            });

            match activity.application_id {
                Some(id) => {
                    activity_payload["id"] = json!(id.to_string());
                },
                None => {}
            }

            match activity.assets {
                Some(assets) => {
                    if let Some(large_text) = assets.large_text {
                            activity_payload["large_text"] = json!(large_text);
                    }

                    if let Some(small_text) = assets.small_text {
                            activity_payload["small_text"] = json!(small_text);
                    }

                    if let Some(large_image) = assets.large_image {
                            activity_payload["large_image"] = json!(large_image);
                    }

                    if let Some(small_image) = assets.small_image {
                            activity_payload["small_image"] = json!(small_image);
                    }
                },
                None => {}
            }

            payload["activities"].as_array_mut().unwrap().push(activity_payload);
        }

        self.presence_sender.send(payload.to_string()).unwrap();
    }
}

Then I've pushed to a channel that broadcasts into websockets.

Websockets

Your implementation will differ here, but the idea is the same.

On the client side, create a WebSocket that onmessage updates the frontend dynamically.

let socket = new WebSocket("/presence");
socket.onopen = function(e) {
    socket.send("OK");
}

const activities_dom = document.querySelector(".discord_activities");
socket.onmessage = function(e) {
    // Update document

    let data = JSON.parse(e.data);
    if (data.heartbeat) {
        return;
    }

    let status = document.getElementById("discord-status");
    if (data.status === "online") {
        status.style.color = "green";

        ...

 

Do note that WebSockets have timeouts, so you're going to have to ping/pong your connection even if nothing updates. I simply send {"heartbeat":1} to the client every 5 seconds.

Then on the server side, create a WebSocket connection by updating the http version to 1.1 and setting the upgrade headers. I'm using nginx so for the presence route:

    location /discord {
        proxy_pass http://127.0.0.1:8080/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

And send the payload to the client.


~Q3 2025~