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}