Discord plugin
Library agnostic Discord parsers and transformers for TagScript.
@tagscript/plugin-discord is what makes TagScript useful inside a Discord bot. It adds tags for embeds, cooldowns, permissions and timestamps, and transformers that let a template read a member, role, channel or guild.
Three rules shape the whole package.
Templates ask, your bot decides. No parser here sends a message, deletes anything or applies a cooldown. Each one records a request on response.actions and renders an empty string. Your code reads that object and picks what to honour.
Structures stay behind a transformer. A member reaches a template through MemberTransformer, which answers with a fixed list of keys. {member.displayName} works, and there is no key that hands back the client, the token, or a method.
Payloads in, payloads out. The only Discord dependency is discord-api-types. Transformers read the raw objects Discord sends, and EmbedParser writes an APIEmbed. Any library that hands you those objects works.
Installation
Install it alongside tagscript:
npm install @tagscript/plugin-discord tagscriptIt needs Node 18 or newer, and ships ESM and CJS builds.
Working with discord.js
Nothing here imports discord.js, so the two meet at the API payload.
On the way in, a transformer wants the raw object, not the wrapper class. discord.js toJSON() will not do: it returns a flattened camelCase blob and turns collections into ID arrays, so roles comes back as a list of IDs with the names gone. Use a payload you already have. Raw gateway events, @discordjs/core, an interaction's data.resolved, and REST calls through client.rest all give you one.
import { Routes, type APIUser } from 'discord-api-types/v10';
import { UserTransformer } from '@tagscript/plugin-discord';
const payload = (await client.rest.get(Routes.user(id))) as APIUser;
await ts.run('Hi {user}!', { user: new UserTransformer(payload) });On the way out, response.actions.embed is an APIEmbed, the same object EmbedBuilder.toJSON() produces. Hand it to EmbedBuilder.from() and send it.
const embed = EmbedBuilder.from(response.actions.embed);Everything below is written against raw payloads, which is what @discordjs/core, Oceanic, Seyfert and a
plain gateway connection hand you. If you are on discord.js, read interaction.data and
interaction.data.resolved off the payload you received rather than the structures discord.js built from
it.
A complete example
import { CooldownParser, DeleteParser, EmbedParser, MemberTransformer, RequiredParser } from '@tagscript/plugin-discord';
import { EmbedBuilder } from 'discord.js';
import { IfStatementParser, Interpreter, StrictVarsParser } from 'tagscript';
const ts = new Interpreter(
new StrictVarsParser(),
new IfStatementParser(),
new EmbedParser(),
new CooldownParser(),
new RequiredParser(),
new DeleteParser(),
);
const response = await ts.run(userWrittenTemplate, {
member: new MemberTransformer(interaction.member),
}, 2_000);Given this template:
{require(Moderator):Moderators only.}
{cooldown(30):Slow down, try again in {retryAfter}.}
{embed(title):Server Rules}
{embed(color):0x37b2cb}
{embed(field):Rule 1|Be nice.|false}
{delete}
Posted by {member.displayName}response.actions comes back as:
{
"require": { "ids": ["Moderator"], "message": "Moderators only." },
"cooldown": { "cooldown": 30, "message": "Slow down, try again in {retryAfter}." },
"embed": {
"title": "Server Rules",
"color": 3650251,
"fields": [{ "name": "Rule 1", "value": "Be nice.", "inline": false }],
},
"deleteMessage": true,
}Nothing has happened yet. Now your handler decides:
const { cooldown, deleteMessage, embed, require: required, silentResponse } = response.actions;
if (required && !matchesAny(interaction.member, required.ids)) {
return interaction.reply(required.message ?? 'You cannot use this tag.');
}
if (cooldown && isOnCooldown(tagName, interaction.user.id)) {
return interaction.reply(cooldown.message ?? 'This tag is on cooldown.');
}
await interaction.reply({
content: silentResponse ? undefined : response.body!,
embeds: embed ? [EmbedBuilder.from(embed)] : [],
});
if (deleteMessage && channel.permissionsFor(client.user).has('ManageMessages')) {
await interaction.channel.messages.delete(triggerMessageId);
}Registering these parsers and ignoring response.actions gives you no access control at all. require
and deny record a list of names your bot has to resolve and check. The plugin never does that for you,
because only your bot knows what a name means in your guild.
Set a charLimit on every run call that handles a template you did not write. The third argument above caps the render at 2000 characters, which also happens to be Discord's message limit.
What is in the package
- Parsers for embeds, cooldowns, permissions, attachments and timestamps.
- Transformers for users, members, roles, channels, guilds and interactions.
- Command options for turning slash command input into variables.
The core parsers and transformers work alongside all of it.
Last updated on