Skip to content

@robojs/prefix - Prefix Commands Plugin #452

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions