Ctrl K
ComunidadGuías…

group-translate/plugin-chat.gateway.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { ChatGateway } from './core/ports';
import type { PluginMessagingCapability, PluginEngineReadCapability } from '../types/openwa';

/**
 * ChatGateway backed by the Tier-2 plugin capability surface: writes go through
 * `ctx.messages` (routed via MessageService, so persistence is preserved), and group-admin
 * reads through `ctx.engine`. It implements the same port the translation core already
 * depends on, so the coordinator/parser/formatter are reused unchanged.
 */
export class PluginChatGateway implements ChatGateway {
  constructor(
    private readonly messages: PluginMessagingCapability,
    private readonly engine: PluginEngineReadCapability,
  ) {}

  // Whatever the translation backend returned is forwarded straight through, and an empty result is a
  // real possibility (a backend that answers 200 with a blank translation). The host now rejects an
  // empty positional argument outright — "argument 2 must be a non-empty string" — so an empty
  // translation stopped being a blank WhatsApp bubble and became a thrown capability error, swallowed by
  // the coordinator's catch. Dropping it here keeps the failure quiet either way, but says why.
  async sendText(sessionId: string, chatId: string, text: string): Promise<void> {
    if (!text.trim()) return;
    await this.messages.sendText(sessionId, chatId, text);
  }

  async sendCombinedReply(sessionId: string, chatId: string, quotedMessageId: string, text: string): Promise<void> {
    if (!text.trim()) return;
    await this.messages.reply(sessionId, chatId, quotedMessageId, text);
  }

  async getGroupAdmins(sessionId: string, chatId: string): Promise<string[]> {
    interface GroupInfoLike {
      participants?: Array<{ id?: string; isAdmin?: boolean; isSuperAdmin?: boolean }>;
      owner?: string;
    }
    const info = (await this.engine.getGroupInfo(sessionId, chatId)) as GroupInfoLike | null | undefined;
    if (!info || !Array.isArray(info.participants)) return [];
    const admins = info.participants
      .filter(p => p?.isAdmin || p?.isSuperAdmin)
      .map(p => p?.id)
      .filter((id): id is string => typeof id === 'string' && id.length > 0);
    // Participant ids can be @c.us while authors arrive @lid; the group `owner` is in the author's scheme,
    // so include it to recognize the creator across that split.
    if (typeof info.owner === 'string' && info.owner) admins.push(info.owner);
    return [...new Set(admins)];
  }

  /**
   * `getContactById` is the bridge across the @lid/@c.us split: handed a `@lid`, the engine looks the
   * contact up in its own store and returns the SAME contact keyed by its canonical `<phone>@c.us` id.
   * (Its `number` field is not that — for a lid contact it is the lid's user part, which is exactly the
   * value that cannot be compared with a participant list.) Read-only and permission-gated by
   * `engine:read`, which this plugin already declares for the admin lookup above.
   *
   * An unknown contact resolves to null. A slow engine or a torn-down session rejects, so the caller
   * can deny this command without remembering the answer it never got.
   */
  async resolveCanonicalWid(sessionId: string, wid: string): Promise<string | null> {
    const contact = (await this.engine.getContactById(sessionId, wid)) as { id?: unknown } | null | undefined;
    const id = contact?.id;
    return typeof id === 'string' && id.length > 0 ? id : null;
  }
}