A slash command looks like ordinary text, but it is not ordinary text.

Consider this message in a multilingual community:

/report msg_481 "personal attack in the second paragraph" 

The sender expects an action. Other participants may need a translation. A moderator needs both the original wording and enough context to make a decision.

The tension is that the same text now has two roles: human-readable language and machine-readable instructions. If translated or reformatted text is sent back through the command parser, a display feature can accidentally become an execution path.

This is similar to a recurring lesson from shell scripting: text that looks harmless can acquire operational meaning when it crosses into the wrong interpreter. The practical response is not “parse more cleverly.” It is to establish a boundary:

Only an authenticated, original message can create a command. Translation is presentation state, never command input.

In this tutorial, we will build that boundary for a Tencent RTC Social Messaging application. The example command creates a moderation report, but it does not automatically punish another user. A human reviewer retains authority over the outcome.

The behavior we want

Our command contract is deliberately narrow:

/report <message-id> "<reason>" 

A reliable implementation must satisfy these rules:

  1. Only original, user-authored text is eligible for parsing.
  2. The syntax is exact and independent of the user's display language.
  3. The target message must be visible to the reporter in the same room.
  4. Duplicate delivery must not create duplicate reports.
  5. Queue failure must produce a retryable state, not a fake success.
  6. Translation may be displayed on demand, but it cannot create or modify the report.
  7. A moderator—not the parser or translation layer—resolves the report.

That gives us three separate concerns:

Original chat event ──> command parser ──> report queue │ │ └──> TUIChat display/translation └──> human review 

Notice the missing arrow: translated display text never returns to the command parser.

Create the TypeScript project

mkdir language-safe-chat-commands cd language-safe-chat-commands npm init -y npm install --save-dev typescript tsx vitest @types/node npx tsc --init mkdir src 

Add these scripts to package.json:

{ "scripts": { "test": "vitest run", "check": "tsc --noEmit" } } 

The core will not depend on a particular UI callback or an invented SDK method. Instead, it accepts trusted application events through a small interface. That makes the dangerous behavior testable before it is connected to a live room.

Represent provenance explicitly

Create src/report-service.ts:

export type MessageProvenance = 'original' | 'derived'; export interface ChatText { messageId: string; roomId: string; authorId: string; contentType: 'text' | 'other'; text: string; provenance: MessageProvenance; } export interface ReportCommand { targetMessageId: string; reason: string; } interface ReportBase { source: ChatText; command: ReportCommand; attempts: number; } export type ReportState = | { status: 'received'; source: ChatText } | { status: 'rejected'; source: ChatText; code: 'INVALID_SYNTAX' | 'TARGET_NOT_VISIBLE'; } | (ReportBase & { status: 'queued'; ticketId: string }) | (ReportBase & { status: 'failed'; code: 'QUEUE_UNAVAILABLE' }) | (ReportBase & { status: 'resolved'; ticketId: string; decision: 'dismissed' | 'actioned'; reviewerId: string; }); export interface TargetPolicy { canReport(input: { roomId: string; reporterId: string; targetMessageId: string; }): Promise<boolean>; } export interface ReportQueue { enqueue(input: { idempotencyKey: string; roomId: string; reporterId: string; targetMessageId: string; originalReason: string; }): Promise<{ ticketId: string }>; } 

provenance is not a value that should be trusted from a browser request. Your server-side integration must derive it from the authenticated event path.

For example, a raw message event accepted from your messaging backend can become original. Text created by translation, summarization, previews, or other transformations must be derived—if it enters this service at all.

An even safer design is to keep translated text entirely outside this event model.

Give the command a small grammar

Using text.split(' ') would make quoted reasons unreliable:

/report msg_481 "personal attack" 

A split would produce four fragments rather than the three logical fields. Instead, use a narrow grammar that accepts a constrained message ID and a quoted reason.

Add this below the interfaces:

type ParseResult = | { kind: 'not-command' } | { kind: 'malformed' } | { kind: 'valid'; command: ReportCommand }; export function parseReport(text: string): ParseResult { // `/reporting` remains ordinary conversation. if (!/^\/report(?:\s|$)/u.test(text)) { return { kind: 'not-command' }; } const match = text.match( /^\/report[ \t]+([A-Za-z0-9_-]{1,64})[ \t]+"((?:\\["\\]|[^"\\]){1,500})"[ \t]*$/u, ); if (!match) { return { kind: 'malformed' }; } const [, targetMessageId, escapedReason] = match; const reason = escapedReason.replace(/\\(["\\])/gu, '$1').trim(); if (!reason) { return { kind: 'malformed' }; } return { kind: 'valid', command: { targetMessageId, reason }, }; } 

This parser makes several product decisions explicit:

  • /report is case-sensitive.
  • Message IDs have a constrained character set and length.
  • Reasons must be quoted.
  • A reason may contain an escaped quote or backslash.
  • Additional flags are rejected rather than guessed.

These restrictions may feel less convenient than natural-language commands, but they reduce ambiguity at a side-effect boundary.

Process commands through explicit states

Continue in src/report-service.ts:

export class ReportService { private readonly states = new Map<string, ReportState>(); constructor( private readonly targets: TargetPolicy, private readonly queue: ReportQueue, ) {} get(messageId: string): ReportState | undefined { return this.states.get(messageId); } async ingest(message: ChatText): Promise<ReportState | undefined> { if (message.contentType !== 'text' || message.provenance !== 'original') { return undefined; } const parsed = parseReport(message.text); if (parsed.kind === 'not-command') { return undefined; } const existing = this.states.get(message.messageId); if (existing) { return existing; } this.states.set(message.messageId, { status: 'received', source: message, }); if (parsed.kind === 'malformed') { const rejected: ReportState = { status: 'rejected', source: message, code: 'INVALID_SYNTAX', }; this.states.set(message.messageId, rejected); return rejected; } const visible = await this.targets.canReport({ roomId: message.roomId, reporterId: message.authorId, targetMessageId: parsed.command.targetMessageId, }); if (!visible) { const rejected: ReportState = { status: 'rejected', source: message, code: 'TARGET_NOT_VISIBLE', }; this.states.set(message.messageId, rejected); return rejected; } return this.enqueue({ status: 'failed', source: message, command: parsed.command, attempts: 0, code: 'QUEUE_UNAVAILABLE', }); } async retry(messageId: string): Promise<ReportState> { const current = this.states.get(messageId); if (!current || current.status !== 'failed') { throw new Error('Only failed reports can be retried'); } return this.enqueue(current); } resolve(input: { messageId: string; reviewerId: string; decision: 'dismissed' | 'actioned'; }): ReportState { const current = this.states.get(input.messageId); if (!current || current.status !== 'queued') { throw new Error('Only queued reports can be resolved'); } const resolved: ReportState = { ...current, status: 'resolved', reviewerId: input.reviewerId, decision: input.decision, }; this.states.set(input.messageId, resolved); return resolved; } private async enqueue( report: Extract<ReportState, { status: 'failed' }>, ): Promise<ReportState> { const attempts = report.attempts + 1; try { const result = await this.queue.enqueue({ idempotencyKey: report.source.messageId, roomId: report.source.roomId, reporterId: report.source.authorId, targetMessageId: report.command.targetMessageId, originalReason: report.command.reason, }); const queued: ReportState = { status: 'queued', source: report.source, command: report.command, attempts, ticketId: result.ticketId, }; this.states.set(report.source.messageId, queued); return queued; } catch { const failed: ReportState = { status: 'failed', source: report.source, command: report.command, attempts, code: 'QUEUE_UNAVAILABLE', }; this.states.set(report.source.messageId, failed); return failed; } } } 

The in-memory map makes the example easy to reproduce, but it is not sufficient for production durability. Persist report state, and require the queue or database to enforce uniqueness on the idempotency key.

That second constraint matters because a process can crash after the queue accepts a report but before the local state is updated. An in-memory duplicate check cannot cover that interval.

Verify the dangerous paths

Create src/report-service.test.ts:

import { describe, expect, it, vi } from 'vitest'; import { type ChatText, type ReportQueue, ReportService, type TargetPolicy, } from './report-service.js'; const original = (text: string): ChatText => ({ messageId: 'command-1', roomId: 'room-7', authorId: 'user-2', contentType: 'text', provenance: 'original', text, }); const allowed: TargetPolicy = { canReport: async () => true, }; describe('ReportService', () => { it('queues a valid command while preserving the original reason', async () => { const enqueue = vi.fn(async () => ({ ticketId: 'ticket-9' })); const service = new ReportService(allowed, { enqueue }); const state = await service.ingest( original('/report msg_481 "personal attack in paragraph two"'), ); expect(state?.status).toBe('queued'); expect(enqueue).toHaveBeenCalledWith({ idempotencyKey: 'command-1', roomId: 'room-7', reporterId: 'user-2', targetMessageId: 'msg_481', originalReason: 'personal attack in paragraph two', }); }); it('does not execute derived or translated text', async () => { const enqueue = vi.fn(); const service = new ReportService(allowed, { enqueue } as ReportQueue); const state = await service.ingest({ ...original('/report msg_481 "translated display text"'), provenance: 'derived', }); expect(state).toBeUndefined(); expect(enqueue).not.toHaveBeenCalled(); }); it('does not duplicate a report when an event is delivered twice', async () => { const enqueue = vi.fn(async () => ({ ticketId: 'ticket-9' })); const service = new ReportService(allowed, { enqueue }); const message = original('/report msg_481 "reason"'); await service.ingest(message); await service.ingest(message); expect(enqueue).toHaveBeenCalledTimes(1); }); it('exposes queue failure and allows an explicit retry', async () => { const enqueue = vi .fn() .mockRejectedValueOnce(new Error('unavailable')) .mockResolvedValueOnce({ ticketId: 'ticket-10' }); const service = new ReportService(allowed, { enqueue }); const failed = await service.ingest( original('/report msg_481 "reason"'), ); expect(failed?.status).toBe('failed'); const retried = await service.retry('command-1'); expect(retried.status).toBe('queued'); expect(enqueue).toHaveBeenCalledTimes(2); }); it('rejects a target the reporter cannot access', async () => { co...
Source:https://dev.to/susiewang/never-execute-the-translation-build-language-safe-slash-commands-for-tencent-rtc-chat-31p4