DEV Community

LunarDrift
LunarDrift

Posted on

Never Execute the Translation: Build Language-Safe Slash Commands for Tencent RTC Chat

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"
Enter fullscreen mode Exit fullscreen mode

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>"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add these scripts to package.json:

{
  "scripts": {
    "test": "vitest run",
    "check": "tsc --noEmit"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }>;
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 },
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 () => {
    const enqueue = vi.fn();
    const denied: TargetPolicy = { canReport: async () => false };
    const service = new ReportService(denied, { enqueue } as ReportQueue);

    const state = await service.ingest(
      original('/report hidden-message "reason"'),
    );

    expect(state).toMatchObject({
      status: 'rejected',
      code: 'TARGET_NOT_VISIBLE',
    });
    expect(enqueue).not.toHaveBeenCalled();
  });
});
Enter fullscreen mode Exit fullscreen mode

Run both checks:

npm test
npm run check
Enter fullscreen mode Exit fullscreen mode

These tests prove the application-level authority boundary. They do not prove that the live integration correctly labels provenance, persists idempotency keys, or keeps translated rendering out of the event stream. Those require integration tests.

Connect the boundary to Tencent RTC Social Messaging

Tencent RTC's Social Messaging solution covers experiences including 1-to-1 chat, group discussion, communities, rich media, and live-room chat. Use the official solution page to select the appropriate messaging surface for your application:

https://trtc.io/solutions/social-messaging

TUIChat also documents on-demand text-message translation. The supported content types, languages, and edition limits are deployment considerations, so verify them against the current official documentation rather than assuming every message can be translated:

https://trtc.io/document/60772

The integration should preserve this direction of data flow:

  1. TUIChat displays the original message.
  2. A user may request the documented translation experience for eligible content.
  3. Translation changes what that user can read; it does not replace the authoritative message event.
  4. Your trusted backend adapter converts an authenticated original message event into ChatText.
  5. ReportService.ingest() classifies and validates it.
  6. The report queue stores the original reason and source message ID.
  7. A moderator reviews the report and records a decision.

Do not attach the parser to rendered DOM text, clipboard text, translated labels, notification previews, or accessibility output. Those surfaces are presentation artifacts and may differ from the original message.

Also avoid inventing a second “translated message” in the room merely to show localized text. If your product deliberately publishes generated text as a new chat message, treat it as a separate derived message and make it ineligible for commands.

What users should see in each state

Internal state is only useful when it produces honest feedback.

State User-visible behavior
received Brief pending indicator; do not claim success
rejected / INVALID_SYNTAX Show the exact accepted format without attempting a partial command
rejected / TARGET_NOT_VISIBLE Say the message cannot be reported from the current context; do not reveal whether a hidden message exists
failed / QUEUE_UNAVAILABLE Keep the command visible and offer retry; do not say “report submitted”
queued Confirm receipt and provide a reference if your policy permits it
resolved Show only the outcome information your moderation policy allows

Translation failure should be reported separately from command state. A failed translation does not mean the report failed, and a successful translation does not mean the report was accepted.

Failure cases worth drilling in staging

A translated string happens to begin with /report

Expected result: it remains display text. No queue call occurs.

Inspect the actual live integration to confirm that translation does not emit another event through the original-message handler.

The sender edits or deletes the command

Choose a policy rather than silently guessing. A defensible default is that a queued moderation report is an immutable record of what was submitted at that time. A later edit can be stored as additional context, but it should not rewrite the original report.

If your product instead allows withdrawal, make withdrawal a separate authenticated operation—not an implication inferred from edited text.

The queue accepts the report and the application crashes

On restart, redelivery may call enqueue again. Verify that the durable queue or database returns the existing record for the same source message ID rather than creating another case.

The target disappears before review

Keep the report and mark the target as unavailable. Deletion may itself be relevant moderation context. Do not automatically resolve the report merely because the current UI can no longer render the target.

A moderator requests a translation

Show the original and translation together. The translated reason can help comprehension, but the reviewer should be able to inspect the source wording and surrounding conversation before deciding.

A malicious client marks its own text as original

This is why provenance cannot be a client-controlled boolean. Build ChatText from a verified server-side message path and authenticated identity. Reject direct browser attempts to call the report queue with arbitrary author or room fields.

When slash commands are the wrong interface

A fixed command grammar is not always the best choice. Use this decision framework:

Interface Best fit Main trade-off
Fixed slash command Developer communities, keyboard-heavy workflows, bot integrations Requires users to learn exact syntax
Report button plus form General consumer communities and sensitive moderation flows More UI work, but far less parsing ambiguity
Localized command aliases Environments where commands must be typed in several languages Alias collisions and a larger test matrix
Natural-language intent parsing Low-risk discovery or suggestions Too ambiguous for direct moderation side effects

For most user-facing moderation systems, a report button with structured fields is safer. The slash command is appropriate when it is intentionally part of the community's interaction model—not merely because parsing text was quicker to demo.

If you do support localized aliases such as multiple words for /report, resolve aliases to a stable command identifier before execution. Keep the target ID and reason structured, and test every alias. Never translate an arbitrary sentence and then ask whether the result resembles a command.

Release verification checklist

Before enabling command processing in a Tencent RTC social-messaging room, verify all of the following:

  • [ ] Original and translated text are separate presentation states.
  • [ ] The parser receives authenticated original events, not rendered text.
  • [ ] Non-text and derived content cannot execute commands.
  • [ ] Quoted spaces and escaped quotes have automated tests.
  • [ ] Malformed commands cause no partial side effects.
  • [ ] The target must belong to a context visible to the reporter.
  • [ ] Queue idempotency survives process restart and event redelivery.
  • [ ] Queue failure remains visibly retryable.
  • [ ] Translation failure does not alter report state.
  • [ ] Moderators can inspect original wording and relevant context.
  • [ ] No automated sanction occurs merely because the command parsed.
  • [ ] Supported translation content, language, and edition requirements have been checked against the current TUIChat documentation.

The useful reframing is that multilingual chat does not have one universal text value. It has original content, translated presentation, machine instructions, and human decisions. Reliability comes from keeping those roles separate rather than asking one string to carry all of them.


Disclosure: I have a content relationship with Tencent RTC. Official Tencent RTC documentation was used as the implementation reference for this article.

Top comments (0)