Plugin pairs
A plugin on the client, the same plugin on the server, and a pipe between them
A client addon changes Gryt for the person running it. A server plugin changes it for everybody on that server. On their own, neither knows the other exists.
A pair is both halves talking. The client half knows something — what you're playing, what you're reading, where you are in a game — and the server half is what makes everybody else's client show it.
If you've written a Minecraft mod that needs to be on the server too, this is the same shape.
What Gryt carries
One thing, in both directions:
{ topic, data }topic is a short routing key. data is whatever you want, up to 8 KB of
JSON. What the two halves say to each other is your protocol, and Gryt stays out
of it.
The client half
// manifest.json needs "capabilities": ["messaging"]
await gryt.messaging.send("playing", { game: "Factorio" });
gryt.messaging.on("playing", ({ host, data }) => {
// Somebody on `host` is playing something.
});It's gryt, not window.gryt: your plugin runs in a worker, and you don't pass
your addon's id because Gryt already knows which plugin is calling.
send goes to every server you're on unless you name one. A server that isn't
running the other half drops it, so sending into nothing is cheap rather than an
error — most servers won't have your plugin.
gryt.messaging.servers() tells you which ones do:
for (const host of await gryt.messaging.servers()) {
gryt.log.info(`${host} runs the other half`);
}It's a promise — the answer lives in the app, and your plugin is on the other side of a message port.
Hosts, and nothing else. There's no version in there on purpose — see versions below.
An empty list means no server you're on is running it, or you're on a server too old to say. Both look the same from here.
The server half
// manifest.json needs "capabilities": ["messaging"]
export function activate(api) {
api.messaging.on("playing", (message) => {
api.messaging.send("playing", {
who: message.nickname,
game: message.data.game,
});
});
}message.userId and message.nickname come from the connection, not from
what was sent. Somebody can't claim to be somebody else.
send goes to everybody by default, or to the members you name:
api.messaging.send("playing", data, [message.userId]);Check what arrives. message.data was written by somebody's client. Gryt
caps how big and how deeply nested it is, and nothing else — the shape is yours
to establish. Assuming your own client half is on the other end is the mistake
this warning exists for.
Being findable
You don't have to do anything. Every plugin a server runs is named to everybody who joins it, along with what it's allowed to do:
{
"id": "automod",
"name": "Automod",
"author": "somebody",
"description": "Bans people posting the same thing everywhere",
"homepage": "https://example.com/automod",
"capabilities": ["messages:read", "moderation"]
}There's no way to turn that off. A server plugin reads the messages people send, and the people sending them get to know what's reading them. An operator who'd rather that weren't visible is the case this exists for.
For your plugin it means gryt.messaging.servers() just works. For everybody on the server
it means the more important thing: the capabilities are there, because "this
server runs automod" tells nobody anything, and "…which reads every message you
send" is a sentence somebody can act on.
No version numbers
The one thing a server won't say is which version it's running. A version number is which known problem applies, and handing that to everybody who joins answers a question an attacker would otherwise have to ask.
If your two halves need to agree on a version, put one in your own payloads — between the two halves rather than on the doorstep.
People are told when they're missing your half
Anybody on a server running your server half, without your client half installed, is told so — in the server menu, under What this server runs:
One of these has a part you do not have. Everything here works without them. You will just not see whatever they add.
Gryt works this out on its own. A server plugin asking for messaging has a
client half by definition, so there's no flag to set — but there's nothing to
click unless you put a homepage in the server half's manifest, and then the
row links to it.
Nobody is turned away for missing it. That's the one place this isn't like a Minecraft mod: the messages and the voice are Gryt, and your plugin is what somebody added on top.
Your link is checked
homepage has to be http or https. A manifest is written by whoever wrote
the plugin, and members are shown this, so a javascript: URL would be a link
injection into every one of their clients. Anything else is dropped and the
plugin still loads.
What Gryt refuses
You don't have to check any of this yourself. A message is dropped before your plugin sees it if:
| the sender hasn't joined | no token, no message |
| the topic isn't a short routing key | up to 64 letters, digits, . - : _ |
| the payload is over 8 KB | measured in bytes, so one emoji is four |
| it nests more than 8 deep | a thousand levels fits in 8 KB, and breaks whatever walks it |
| it holds more than 512 values | 8 KB of {"a":1,"b":1,…} is thousands of keys |
it has a __proto__, constructor or prototype key | the obvious deep merge walks straight into it |
| somebody sends more than 30 in ten seconds | per person, per plugin |
Everything past that is yours. Gryt checks the structure; you check the meaning.
A pair that works
Both halves of a plugin that shows what people are playing.
The server half
On the server, in your plugins folder, presence/manifest.json:
{
"id": "presence",
"name": "Presence",
"version": "1.0.0",
"main": "index.mjs",
"author": "you",
"description": "Shows what people are playing",
"capabilities": ["messaging", "members:read"]
}And presence/index.mjs:
// Who is playing what. Lost on restart, which is fine — every client sends
// again when it reconnects.
const playing = new Map();
export function activate(api) {
api.messaging.on("playing", (message) => {
// Somebody else's bytes. A game name is a string or it is nothing.
const game = typeof message.data?.game === "string" ? message.data.game.slice(0, 80) : null;
if (game) playing.set(message.userId, { who: message.nickname, game });
else playing.delete(message.userId);
api.messaging.send("everyone", [...playing.values()]);
});
// Somebody left, so stop telling people they are playing something.
api.on("member:left", (member) => {
if (!playing.delete(member.userId)) return;
api.messaging.send("everyone", [...playing.values()]);
});
}The client half
In your addons folder, presence/manifest.json:
{
"id": "presence",
"name": "Presence",
"version": "1.0.0",
"type": "plugin",
"main": "index.js",
"capabilities": ["messaging"]
}And presence/index.js:
export function activate() {
gryt.messaging.on("everyone", ({ host, data }) => {
// `data` came from your own server half, which built it from other
// people's messages. Still worth checking.
if (!Array.isArray(data)) return;
for (const entry of data) {
gryt.log.info(`[${host}] ${entry.who} is playing ${entry.game}`);
}
});
// Tell the servers that are listening.
const say = async (game) => {
for (const host of await gryt.messaging.servers()) {
await gryt.messaging.send("playing", { game }, host);
}
};
say("Factorio");
}Turn both on
Restart the server, then enable the addon in Settings, Addons, and allow Exchange its own messages with the servers you are on.
The server log tells you the half it loaded:
ℹ plugin presence 1.0.0 started (messaging, members:read)Everybody on that server can now see presence in the list of what it runs,
and that it reads who joins and leaves.
The client half in that example asks the person nothing — it says Factorio and means it. Reading what you're actually running isn't possible yet; a plugin can't see your processes, and the client addons page says why. Until that changes, a real one would ask you to type it, or read it from a game that publishes it somewhere a web page can reach.
Versions
Two people will be on different versions of your plugin the day after you publish it, and Gryt won't help you with that — the server doesn't say which version it's running, deliberately.
So put one in your own payloads and ignore what you don't understand:
gryt.messaging.send("playing", { v: 1, game });That's between your two halves, which is where it belongs.
When nothing happens
gryt.messaging.servers() is empty. No server you're on is running the other half, or
you're on a server old enough not to say. Check the server log says your plugin
started — and check the server menu, under What this server runs, which
lists what that server admits to.
Messages send and nothing comes back. The server drops a message for a plugin it isn't running, silently and on purpose. Check the server log says your plugin started.
gryt.messaging calls reject. The addon's manifest is missing
"capabilities": ["messaging"], or the switch under it in Settings is off. Both
have to be true. The message says which.
Nothing arrives after a while. Thirty messages per ten seconds is the limit.
Past that the client gets a plugin:error and the message is dropped.
What this doesn't protect you from
The two halves are not the same shape here, and it is worth knowing which is which.
The client half runs in a worker. It can reach the gryt API for what it was
granted, and the internet, and nothing else — not the DOM, not your messages,
not your identity key.
The server half runs in the server process, with the server's own database and filesystem, and nothing contains it. That is what makes it useful and it is also the whole of the risk.
Both keep their internet connection either way, so what either half is given it can send anywhere. A pair is two pieces of somebody else's code, one on your machine and one on the server's. Install ones you'd trust the author with.