Sharkord
Plugins

Messages

Send, edit, read, pin and react from a plugin.

ctx.messages lets a plugin act in text channels on its own behalf. Messages sent this way are authored by the plugin, not by a user: they render with your name and logo and a bot badge, and nobody can edit them in the client.

Send

import type { PluginContext } from "@sharkord/plugin-sdk";

const onLoad = (ctx: PluginContext) => {
  ctx.events.on("user:joined", async ({ username }) => {
    const { messageId } = await ctx.messages.send(
      1,
      `<p>Welcome, ${username}!</p>`,
    );

    ctx.logger.debug("sent welcome message", messageId);
  });
};
send(
  channelId: number,
  content: string,
  options?: {
    parentMessageId?: number;
    replyToMessageId?: number;
    files?: { name: string; data: Uint8Array }[];
    previews?: boolean;
  }
): Promise<{ messageId: number }>
  • parentMessageId posts the message as a thread reply. Threads are one level deep, so the parent cannot itself be a reply.
  • replyToMessageId posts it as an inline reply.
  • Both must point at a message in the same channel.
  • files attaches uploads, capped by the server's per-message file limit. A message carrying files may have empty content.
  • previews controls whether the host looks up the links in the message. On by default.

By default the server fetches the URLs in a message and renders link cards, plus an inline image or video for a link that points at one. previews: false turns that off, and the message renders exactly as written:

await ctx.messages.send(channelId, content, { previews: false });

Worth doing when the message is already formatted the way you want it, or when you would rather the server not fetch the URL at all: a link pulled off an RSS feed or a webhook is a URL you did not choose, and the preview is your server requesting it.

An edit looks the links up again, so pass it there too or a message sent without cards gets them back:

await ctx.messages.edit(messageId, content, { previews: false });

Edit, Delete and Read

await ctx.messages.edit(messageId, "<p>Updated</p>");
await ctx.messages.edit(messageId, "<p>Updated</p>", { previews: false });
await ctx.messages.delete(messageId);

const message = await ctx.messages.get(messageId);

const recent = await ctx.messages.list({ channelId: 1, limit: 50 });

edit and delete only work on the plugin's own messages; anything else fails with a permission error. Deleting a message also deletes the files attached to it. Users cannot edit plugin messages, but a user with MANAGE_MESSAGES can delete them.

get and list read any message. list returns newest first, at most 100 at a time, and takes channelId, limit, before and parentMessageId. Pass parentMessageId to read a thread, and before to page backwards through history.

Reading Attachments

A message's attachments are in its files, and ctx.files.getPath turns a file id into the path of the stored file on disk:

import fs from "node:fs/promises";

const message = await ctx.messages.get(messageId);

for (const file of message?.files ?? []) {
  const filePath = await ctx.files.getPath(file.id);

  if (!filePath) continue;

  // read it, stream it, or hand the path to ffmpeg
  const bytes = await fs.readFile(filePath);
}

getPath returns undefined for an id that does not exist. The file belongs to the host: never write to it, and copy it into dataPath if you need it after the message is gone, since deleting a message deletes its files. file.originalName, mimeType and size are on the file row already, so check them before reading anything.

It works for any stored file, not only attachments: emojis and avatars have file ids too.

Pin and React

await ctx.messages.pin(messageId);
await ctx.messages.unpin(messageId);

await ctx.messages.react(messageId, "👍");
await ctx.messages.unreact(messageId, "👍");

Reacting is done as the plugin, not on a user's behalf, so your reaction sits beside a user's identical emoji as its own. Reacting twice with the same emoji does nothing. Thread replies cannot be pinned.

Content Format

content is HTML, and it is sanitized before it is stored. Only a small set of tags survives:

p, br, strong, em, code, pre, a, and span/img for emoji and mentions.

Everything else, including scripts, styles, and arbitrary attributes, is stripped. A message that ends up empty after sanitizing is rejected unless it carries files.

Wrap your text in <p> and escape anything that came from a user before you interpolate it. Sanitizing protects the server, not your formatting.

Notes

  • These calls raise the usual events, including for your own plugin. Check pluginId in your handlers or you can build a loop.
  • Plugin messages do not run the onBeforeMessageSave hook. A moderation plugin sees what people send, not what plugins send.
  • The plugin must be enabled; calls from a disabled plugin are rejected.
  • ctx.messages is also available in onUnload, so a plugin can post a goodbye or clean up its own messages on the way out.

On this page