Rich Presence
A game, a tool or a script can tell the person's friends what they are doing in it. The card shows on their profile and in member lists, under your application's name and icon, with up to two lines of text, pictures, a timer, a party count and two links.
It runs on the person's own computer. Your program talks to the ILLEST desktop app over a local pipe, and the app puts the card on the person's profile. Nothing goes over a network from your program, and there is no token or secret: the application id is public, and the person decides.
- The first time your application speaks, ILLEST asks the person whether to show it. Until they allow it, nothing is shown.
- Settings → Activity → Programs on this computer lists every application that has asked, with Allow and Block, and one switch that turns it all off.
- Only accepted friends see the card, and nobody sees it while the person is invisible or has paused their activity.
1. Register an application
Open Settings → Developers in ILLEST, or the developer portal, and press New application under Rich Presence applications. Give it a name and an icon: the card always shows these, never a name your program sends.
Add images by key, for example caves or logo. Your program names a picture by its key. Copy the application id.
2. Install the library
pip install illest3. Say what the person is doing
import time
from illest.rpc import Presence
def main():
rpc = Presence("1234567890123456789") # your application id
rpc.connect()
rpc.update(
details="Exploring the caves",
state="Level 4",
start=time.time(),
large_image="caves",
large_text="The Deep Caves",
buttons=[{"label": "Play", "url": "https://example.com/play"}],
)
input("Showing on your profile. Press Enter to stop.")
rpc.close()
if __name__ == "__main__":
main()Run it with the ILLEST desktop app open and signed in. Allow the application when ILLEST asks, and your friends see the card.
Every update() replaces the one before it. clear() takes the card away, and so does close() or your program ending.
In async code
AsyncPresence has the same methods as coroutines, and works as an async context manager:
import asyncio
from illest.rpc import AsyncPresence
async def main():
async with AsyncPresence("1234567890123456789") as rpc:
await rpc.update(details="In the lobby", party_size=[1, 4])
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(main())If the app is closed or restarts, both reconnect by themselves and send the last activity again. update() while the app is away
keeps the activity and returns None. Pass auto_reconnect=False to get ConnectionLost instead.
What update() takes
| Argument | What it is |
|---|---|
details | The first line, up to 128 characters. |
state | The second line, up to 128 characters. With party_size, the count follows it: "In a squad (2 of 4)". |
start | When it started, as Unix seconds, milliseconds or a datetime. The card counts up from it. |
end | When it ends, at most a day ahead. Alone it counts down; with start the card draws a progress bar. |
large_image | An image key of your application, or an https:// address. With none, the card shows your icon. |
large_text | Shown when the large picture is hovered. |
small_image, small_text | A small round picture on the large one's corner, and its hover text. |
party_size | [size, most], for example [2, 4]. |
buttons | Up to two {"label": ..., "url": ...}. Labels up to 32 characters; addresses are http or https only. |
activity_type | ActivityType.playing (the default), listening, watching or competing. |
A picture given by address is fetched once by the desktop app and kept on the person's ILLEST server, so nobody who sees the card asks your site for it. A key your application does not have shows no picture, rather than refusing the update.
The library checks every argument before sending, so a mistake raises ValueError or TypeError at the line that made it.
Limits
- Five updates every twenty seconds per connection. Past that, the newest update waits and is shown as soon as the window allows; older waiting ones are dropped. It is never an error, so update as often as your program changes state.
- Sixteen programs can be connected at once.
- A frame is at most 64 KiB.
Errors
All are in illest.rpc and derive from RPCError, which carries the code the app sent.
| Error | When |
|---|---|
NotRunning | No ILLEST desktop app is listening on this computer. |
HandshakeRefused | The app turned you away at hello: Rich Presence is off (4006), the id is malformed (4000), or too many programs are connected. |
Blocked (4006) | The person blocked your application, or turned Rich Presence off while you were connected. |
UnknownApplication (4007) | No application has this id. |
InvalidActivity (4000) | The app refused the activity, which a client other than this library can cause. |
NotConnected | A call before connect(), or after close(). |
ConnectionLost | The app went away and auto_reconnect is off. |
The protocol
To write a client in another language, this is all of it.
Where. On Windows, the named pipe \\.\pipe\illest-ipc-0; if that is taken, -1 to -9. Elsewhere, a Unix socket
illest-ipc-0 (to -9) in $XDG_RUNTIME_DIR, else $TMPDIR, else /tmp. Try each in turn. The pipe refuses connections from
other computers, and the socket is readable by its user only. The environment variable ILLEST_IPC_PATH points the library at
one path instead.
Framing. Every message is an opcode and the payload's length, each a 32-bit little-endian unsigned integer, then the payload as UTF-8 JSON.
| Opcode | Name | Payload |
|---|---|---|
| 0 | HANDSHAKE | {"v": 1, "client_id": "<application id>"}, the first thing you send. |
| 1 | FRAME | A command, or the app's answer. |
| 2 | CLOSE | {"code": ..., "message": ...}. Either side may send it; the connection ends. |
| 3 | PING | Anything; the app answers PONG with the same payload. |
| 4 | PONG | The echo. |
Hello. Send HANDSHAKE within ten seconds of connecting. The app answers with a FRAME:
{"cmd": "DISPATCH", "evt": "READY", "data": {"v": 1, "config": {"app": "illest"}}, "nonce": null}or with CLOSE and a code. READY says nothing about the person.
Commands. Each FRAME you send carries a nonce of your choosing, which the answer repeats.
{"cmd": "SET_ACTIVITY", "nonce": "1", "args": {"pid": 4242, "activity": {
"type": "playing", "details": "Exploring the caves", "state": "Level 4",
"timestamps": {"start": 1793450000},
"assets": {"large_image": "caves", "large_text": "The Deep Caves"},
"party": {"size": [2, 4]},
"buttons": [{"label": "Play", "url": "https://example.com/play"}]
}}}type is playing, listening, watching or competing (or 0, 2, 3 and 5). activity: null clears, as does
{"cmd": "CLEAR_ACTIVITY", "nonce": "2", "args": {"pid": 4242}}.
The answer is {"cmd": "SET_ACTIVITY", "data": <the activity as taken>, "evt": null, "nonce": "1"}. A refusal has
"evt": "ERROR" and data {"code": ..., "message": ...}:
| Code | Meaning |
|---|---|
| 4000 | The activity or the hello is malformed, or breaks a limit. |
| 4002 | A command other than SET_ACTIVITY and CLEAR_ACTIVITY. |
| 4006 | Blocked by the person, or Rich Presence is off. |
| 4007 | No application has this id. |
Close the connection, or send CLOSE, and the card goes with it.