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