Skip to content

@robojs/prefix - Prefix Commands Plugin #452

@Pkmmte

Description

@Pkmmte

Summary

A Robo.js plugin that lets communities keep using prefix commands (e.g., !ping) while they migrate to slash commands. It scans a project’s /src/prefix-commands folder, routes messageCreate events to handlers, supports Sage-like return semantics (return a value → bot replies), and shows a “thinking…” placeholder if execution exceeds 300 ms, later editing it with the final result or error.

⚠️ Note: Prefix commands require the Message Content privileged intent. See the References section.

Goals

  • Provide a simple, pragmatic bridge for legacy prefix commands without blocking migration to slash commands.
  • Mirror Robo ergonomics: return a value to reply (Sage-like), minimal boilerplate, and clear file structure.
  • Encourage slash commands in the README and examples.

Requirements & Constraints

  • Discord Intents: Message Content is required to parse prefixes from message text. Apps at scale must apply for privileged intents via the Developer Portal. Smaller bots can enable them directly (see References).
  • Event Source: Subscribe to messageCreate; only process when content begins with a configured prefix or bot mention.
  • Rate Limits: Respect Discord rate limits when sending and editing messages.
  • Compatibility: Designed for the current discord.js API and Robo.js plugin model.

Scope & Features

  • Prefix parsing with support for multiple prefixes (global config) and case-insensitive matching.
  • Folder-based command loading from /src/prefix-commands/** (using node:fs), mirroring Robo’s file-based philosophy.
  • Sage-like return semantics: command handlers may simply return strings/embeds to reply; throwing yields a friendly error reply.
  • Thinking/deferral UX: If a handler takes >300 ms, send a placeholder (or typing indicator) and edit with the final result.

API Design

1) Plugin Configuration (file: config/plugins/prefix.mjs)

// config/plugins/prefix.mjs
export default {
  // Global defaults
  prefixes: ['!'],                 // supports multiple, e.g., ['!', '?']
  directory: 'src/prefix-commands',
  mentionPrefix: true,             // allow "@Bot ping"
  caseInsensitive: true,
  allowDMs: false,

  // Thinking behavior
  thinking: {
    strategy: 'message',           // 'off' | 'typing' | 'message'
    delayMs: 300,
    placeholder: '…thinking…'
  },

  deleteTrigger: false,            // optionally delete the user’s trigger message

  errorStyle: {
    showStack: false,
    message: 'Something went wrong running that command.'
  },

  parsing: {
    quotes: true,                  // keep "quoted strings" intact
    greedyLastArg: true            // last arg captures the rest of the line
  }
}

2) Imperative API (for programmatic control)

// @robojs/prefix — minimal public API
export interface PrefixPluginConfig {
  prefixes: string[];
  directory: string;               // default: 'src/prefix-commands'
  mentionPrefix: boolean;
  caseInsensitive: boolean;
  allowDMs: boolean;
  thinking: { strategy: 'off' | 'typing' | 'message'; delayMs: number; placeholder: string };
  deleteTrigger: boolean;
  errorStyle: { showStack: boolean; message: string };
  parsing: { quotes: boolean; greedyLastArg: boolean };
}

export const Prefix = {
  getGlobalConfig(): {} as PrefixPluginConfig,
  reload: async () => { /* re-scan handlers from disk */ }
} as const;

3) Handler Authoring API (in /src/prefix-commands) (in /src/prefix-commands)

Handlers mirror the ergonomics of Robo slash commands: return a value to reply. When you need full control, use provided helpers.

// src/prefix-commands/ping.ts
import type { LegacyContext, LegacyResult } from '@robojs/prefix';

export default async function handler(ctx: LegacyContext): Promise<LegacyResult> {
  // Sage-like: return a value → plugin replies (or edits the thinking message)
  return `Pong! (latency ~${Date.now() - ctx.message.createdTimestamp}ms)`;
}
// Types exposed to handler authors
export interface LegacyContext {
  message: import('discord.js').Message;
  args: string[];
  userId: string;
  // helpers
  reply: (payload: ReplyPayload) => Promise<void>;      // manual reply
  think: () => Promise<void>;                           // emit placeholder now
  edit: (payload: ReplyPayload) => Promise<void>;       // edit placeholder
}

export type ReplyPayload =
  | string
  | { content?: string; embeds?: any[]; components?: any[]; files?: any[] };

export type LegacyResult = void | ReplyPayload | Promise<ReplyPayload | void> = void | ReplyPayload | Promise<ReplyPayload | void>;

Architecture & Implementation Details

A) File Scanning

  • Use node:fs to recursively scan directory (src/prefix-commands) and build a registry: commandName -> module path.

  • Resolution rules:

    • src/prefix-commands/ping.ts!ping
    • src/prefix-commands/mod/kick.ts!mod kick (nested groups)
    • Case sensitivity controlled by caseInsensitive.

B) Event Flow (messageCreate)

  1. Ignore bots and messages with no content.

  2. Check for prefix or mention match. Extract cmd + args (with quotes and greedy last arg options).

  3. Resolve handler; check guild-level blocks and allowDMs.

  4. Start a 300 ms timer:

    • If still pending after delayMs:

      • typing: call channel.sendTyping() (renew as needed).
      • message: send a placeholder message and keep its reference.
  5. Execute handler with LegacyContext. On return:

    • If placeholder exists → edit it with final payload.
    • Else → reply to the trigger message.
  6. On throw: reply with errorStyle.message (and optionally stack when configured). Ensure edits respect rate limits.

C) Rate Limits & Reliability

  • Coalesce progress into one edit when possible; avoid spamming edits.
  • Honor Discord rate-limit guidance for sends/edits.
  • Provide minimal retries with jitter only on transient failures.

D) Security, Privacy, and Intents

  • Clearly document Message Content requirements and obtain only necessary intents.
  • Don’t process DMs unless allowDMs is enabled.
  • Avoid logging raw message content in production unless you use logger.debug().

Usage Examples

1) Simple Ping

// src/prefix-commands/ping.ts
export default () => 'Pong!';

2) Long Task with Explicit Thinking

// src/prefix-commands/report.ts
export default async ({ think }: LegacyContext) => {
  await think();                           // force placeholder immediately
  const data = await makeHeavyCall();
  return { content: `Report ready:\n${format(data)}` };
}

Suggested Project Layout (plugin repository)

@robojs/prefix/
  src/
    index.ts              # plugin entry (register event + config load)
    registry.ts           # fs-based scan of /src/prefix-commands → map name->module
    router.ts             # prefix/mention parsing, arg tokenizer
    executor.ts           # 300ms thinking, run handler, auto-reply/edit
    types.ts              # LegacyContext, ReplyPayload, public API types
  README.md
  package.json

Testing (Encouraged, Not Required)

  • Unit: parser, registry resolution.
  • Integration: fake messageCreate events; assert reply/edit behavior under/over 300 ms.
  • E2E (optional): spin up a test guild with a bot token; verify !ping → “Pong!” and the thinking/edit flow.

Contributing / Acceptance Criteria

We’ll accept the PR when:

  • !ping works end-to-end with Sage-like return semantics.
  • 300 ms thinking/edit flow functions as configured.
  • README includes Message Content instructions and migration guidance.

Nice-to-haves:

  • Example commands (ping, report, args-demo).
  • Basic unit tests and a short demo GIF in the PR.

References

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions