main
  1import { client, xml, jid } from "@xmpp/client";
  2import type { Element } from "@xmpp/xml";
  3import { XmppMessage, bareJid } from "./types.js";
  4import { Config } from "../config.js";
  5
  6export type MessageHandler = (message: XmppMessage) => Promise<void>;
  7
  8export class XmppClient {
  9  private xmpp: ReturnType<typeof client>;
 10  private messageHandler: MessageHandler | null = null;
 11  private config: Config;
 12
 13  constructor(config: Config) {
 14    this.config = config;
 15
 16    this.xmpp = client({
 17      service: `xmpp://${jid(config.xmpp.jid).domain}`,
 18      username: jid(config.xmpp.jid).local,
 19      password: config.xmpp.password,
 20    });
 21
 22    this.setupEventHandlers();
 23  }
 24
 25  private setupEventHandlers(): void {
 26    this.xmpp.on("error", (err: Error) => {
 27      console.error("XMPP error:", err.message);
 28    });
 29
 30    this.xmpp.on("offline", () => {
 31      console.log("XMPP offline");
 32    });
 33
 34    this.xmpp.on("online", async (address: ReturnType<typeof jid>) => {
 35      console.log(`XMPP online as ${address.toString()}`);
 36
 37      // Send initial presence
 38      await this.xmpp.send(xml("presence"));
 39    });
 40
 41    this.xmpp.on("stanza", async (stanza: Element) => {
 42      if (stanza.is("message")) {
 43        await this.handleMessage(stanza);
 44      }
 45    });
 46  }
 47
 48  private async handleMessage(stanza: Element): Promise<void> {
 49    const type = stanza.attrs.type as "chat" | "groupchat" | undefined;
 50
 51    // Only handle chat messages (direct messages)
 52    if (type !== "chat") {
 53      return;
 54    }
 55
 56    const body = stanza.getChildText("body");
 57    if (!body) {
 58      return;
 59    }
 60
 61    const from = stanza.attrs.from as string;
 62    const to = stanza.attrs.to as string;
 63    const id = stanza.attrs.id as string | undefined;
 64
 65    // Security: only respond to owner
 66    if (bareJid(from) !== bareJid(this.config.xmpp.ownerJid)) {
 67      console.log(`Ignoring message from unauthorized JID: ${bareJid(from)}`);
 68      return;
 69    }
 70
 71    const message: XmppMessage = {
 72      from,
 73      to,
 74      body,
 75      type: "chat",
 76      id,
 77    };
 78
 79    if (this.messageHandler) {
 80      try {
 81        await this.messageHandler(message);
 82      } catch (err) {
 83        console.error("Error handling message:", err);
 84        await this.sendMessage(from, `Error: ${(err as Error).message}`);
 85      }
 86    }
 87  }
 88
 89  onMessage(handler: MessageHandler): void {
 90    this.messageHandler = handler;
 91  }
 92
 93  async sendMessage(to: string, body: string): Promise<void> {
 94    const message = xml(
 95      "message",
 96      { type: "chat", to },
 97      xml("body", {}, body)
 98    );
 99    await this.xmpp.send(message);
100  }
101
102  async start(): Promise<void> {
103    console.log("Starting XMPP client...");
104    await this.xmpp.start();
105  }
106
107  async stop(): Promise<void> {
108    console.log("Stopping XMPP client...");
109    await this.xmpp.stop();
110  }
111}