import { client, xml } from "@xmpp/client"; // import debug from "@xmpp/debug"; import config from "./config.js"; import Lndhub from "./lib/lndhub.js"; import { randomHex, pingXml, isPingResponse, roomDirectMessageXml, grantVoiceMessageXml, parseForm, isVoiceRequest, } from "./lib/utils.js"; const xmpp = client(config.xmpp); // debug(xmpp, true); const connectRetryInMilliseconds = 5000; const pingIntervalInMilliseconds = 3000; const pingTimeoutInMilliseconds = 10000; let pingInterval = null; let lastPingSuccess = null; const lndhub = new Lndhub(config.lndhub.endpoint); function checkAndSendPing () { const now = new Date(); if ((now.getTime() - lastPingSuccess.getTime()) > pingTimeoutInMilliseconds) { console.log(`No ping response received in ${pingTimeoutInMilliseconds/1000} seconds, reconnecting`); clearInterval(pingInterval); xmpp.stop().catch(console.error); } else { xmpp.send(pingXml(config.xmpp.service)) .catch(err => console.warn(err)); } } function startPingInterval () { if (pingInterval) { clearInterval(pingInterval); } pingInterval = setInterval(checkAndSendPing, pingIntervalInMilliseconds); } function waitForInvoicePaid (room, nick, invoice, retries=0) { let timeout = 3000; if (retries >= 300) { console.log(`Invoice not paid after 15 minutes: ${invoice.payment_hash}`); let timeout = 5000; if (retries >= 500) { // TODO ask user to request voice again to create a new invoice return false; } } lndhub.getInvoice(invoice.payment_hash).then(async data => { if (data && data.is_paid) { console.log(`Invoice paid: ${invoice.payment_hash}`); await grantVoice(room, nick); } else { if (!data) console.warn('Fetching invoice status failed'); setTimeout(waitForInvoicePaid, timeout, room, nick, invoice, retries++); } }); } async function respondToVoiceRequest (room, nick, invoice) { const welcomeMessage = ` Welcome, ${nick}! I see you're interested in joining the conversation. You can wait for a human moderator to approve your request, but it may take a while. You may also prove that you're not a spambot by donating a small amount of sats for the maintenance of this chat server via the following Lightning invoice. I will immediately approve your request in that case. ${invoice.payment_request} `; await xmpp.send(roomDirectMessageXml(room, nick, welcomeMessage)); } async function grantVoice (room, nick) { console.log(`Granting voice to ${nick} in ${room}`); await xmpp.send(grantVoiceMessageXml(room, nick)); const message = `Thanks ${nick}, and welcome aboard! You can send messages to the channel now.`; await xmpp.send(roomDirectMessageXml(room, nick, message)); } async function handleVoiceRequest (stanza) { try { const formFields = parseForm(stanza); const room = stanza.attrs.from; // const jid = formFields.find(f => f.name === "muc#jid").value; const nick = formFields.find(f => f.name === "muc#roomnick").value; console.log(`${nick} requested voice in ${room}`); const invoice = await lndhub.createInvoice(config.amounts.voice, `Donation for ${room}`); if (invoice) { console.log(`Created lightning invoice for ${nick}: ${invoice.payment_hash}`) await respondToVoiceRequest(room, nick, invoice); waitForInvoicePaid(room, nick, invoice); } else { console.warn(`Invoice creation failed!`); // TODO notify ops contact } } catch(err) { console.error(err); } } async function connectXmpp () { await xmpp.start().catch(err => { console.log(`Connect error: ${err}`); console.log(`Retrying in ${connectRetryInMilliseconds/1000} seconds`); setTimeout(async () => { await connectXmpp(); }, connectRetryInMilliseconds); }); } function connectLndhub () { return lndhub.auth(config.lndhub.username, config.lndhub.password).then(connected => { if (connected) { console.log("Connected to Lndhub"); // automatically refresh auth token once a day setInterval(() => lndhub.reauth(), 86400000); } else { process.exit(1); } }).catch(err => { console.error(`Lndhub connection failed: ${err}`); }); } function handlePingResponse (stanza) { lastPingSuccess = new Date(); } function readIq (stanza) { if (isPingResponse(stanza)) handlePingResponse(stanza); } function readMessage (stanza) { if (isVoiceRequest(stanza)) handleVoiceRequest(stanza); } xmpp.on("error", (err) => { console.error(err); }); xmpp.on("offline", () => { console.log("Offline"); connectXmpp(); }); xmpp.on("online", async (address) => { console.log("Connected to XMPP server"); for (const jid of config.xmpp.rooms) { const msg = xml("presence", { to: `${jid}/${config.xmpp.nick}` }); await xmpp.send(msg); } connectLndhub(); lastPingSuccess = new Date(); startPingInterval(); }); xmpp.on("stanza", async (stanza) => { if (stanza.is("iq")) readIq(stanza); if (stanza.is("message")) readMessage(stanza); }); xmpp.on("status", (status) => { console.debug("XMPP status:", status); }); connectXmpp();