diff --git a/index.js b/index.js index 9296653..50a5467 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,8 @@ const os = require('os'); const path = require('path'); const wspf = require('websocket-polyfill'); const WebSocket = require('ws'); -const xmpp = require('simple-xmpp'); +const { client, xml } = require('@xmpp/client'); // nostr things let { @@ -18,7 +18,6 @@ let { let letsCID; let signedEvent; - // config setup for home folders // Determine the platform-specific config path function getConfigPath() { @@ -40,184 +39,214 @@ const configPath = getConfigPath(); if (fs.existsSync(configPath)) { console.log(`Found config at ${configPath}`); const config = ini.parse(fs.readFileSync(configPath, 'utf-8')); - // get users nostr private hex let sk = config.nostr.pkhex; - // calculate pubkey from private let pk = nostr.getPublicKey(sk); - // only allow users from config.ini -> [authorized_users] -> users - const authorizedUsers = config.authorized_users.users - ? config.authorized_users.users.split(',').map(user => user.trim()) - : []; - - // We've got a connection to XMPP! - xmpp.on('online', function (data, to) { - console.log('Connected with JID: ' + config.xmpp.jid); - console.log('Pubkey: ' + pk); - }); - - // We've got an XMPP message! - xmpp.on('chat', function (from, message) { - // setup debugging based on config.ini - function debug() { - if (config.debug === '1') { - console.log('[DEBUG]'); - console.log(message); - console.log(from); - }; - } - // deny users not in config.ini - if (!authorizedUsers.includes(from)) { - xmpp.send(from, 'Not authorized'); - debug(); - return; // Exit the function early - } - - // get the latest documentation for nostrsms - if (message === '!help') { - xmpp.send(from, 'https://wiki.vanderwarker.family/doku.php?id=code:nostrsms:commands'); - debug(); - - // get latest 10 posts from global - } else if (message === "!g") { - xmpp.send(from, `Here are the latest 10 posts from global on ${config.relays.read}`); - async function getGlobalEvents() { - // use read relays in config.ini - const relay = nostr.relayInit(config.relays.read, WebSocket); - // connect to read relay - relay.on('connect', () => { - console.log(`connected to ${relay.url}`); - }); - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`); - }); - - await relay.connect(); - - // create subscription to get events - let sub = relay.sub([{ - kinds: [1], - limit: 10 - }, ]) - // we've go an event! time to parse! - sub.on('event', event => { - console.log('got event:', event); - - // Check if the event is already an object - const globalEvents = typeof event === 'string' ? JSON.parse(event) : event; - // if globalEvents.* is set, set to const for later use - if (globalEvents && globalEvents.content && globalEvents.pubkey && globalEvents.created_at) { - const content = globalEvents.content; - const author = globalEvents.pubkey; - const createdAt = globalEvents.created_at; - - // WIP move to debug - console.log('Content:', content); - console.log('Author:', author); - console.log('Created At:', createdAt); - - // send the latest posts to the user whom requested them - xmpp.send(from, `\"${content}\", - ${author} @ ${createdAt}`); - relay.close(); - } else { - console.error('Invalid event structure or missing required properties.'); - } - }); - } - - // let's run the above code to get our events and send them on XMPP/SMS! :) - getGlobalEvents().catch((error) => { - console.error(error); - }); - - // Well they didn't use a command, so let's get to the NOSTR STUFF!!1! - } else { - async function newPost() { - // get relays from config.ini - const relay = nostr.relayInit(config.relays.write, WebSocket); - // look at that, already on the nostr - relay.on('connect', () => { - console.log(`connected to ${relay.url}`); - }); - // dang...failed connection! Try again sooon, or try one of the thousands of other relays ;) - relay.on('error', () => { - console.log(`failed to connect to ${relay.url}`); - }); - - // wait for the above to give us the go ahead - await relay.connect(); - - // let's construct our "note"! - let event = { - // https://git.vanderwarker.family/nostr/nips/src/branch/master/01.md - kind: 1, - // get users public key hex - pubkey: pk, - // create this instant! (timestamp) - created_at: Math.floor(Date.now() / 1000), - // add client tag for UIs to optionally display - tags: [['client','NostrSMS','31990:9be1b8315248eeb20f9d9ab2717d1750e4f27489eab1fa531d679dadd34c2f8d:1726937516']], - // set the nostr content to the incoming xmpp message - content: message, - }; - // sign nostr event - const signedEvent = nostr.finishEvent(event, sk); - // get nostr event it - letsCID = signedEvent.id; - showSID(letsCID, pk); - // publish! - await relay.publish(signedEvent); - // done here. we can disconnect! (from nostr relay) - relay.close(); - } - // catch errors - newPost().catch((error) => { - console.error(error); - }); - - // get's id, pubkey, kind, from signed event, and relay from config.ini and create - // a nip-19 encoded entity to send back to the user - // this can be pasted into most nostr clients for immediate fetching/viewing - async function showSID(letsCID, pk) { - console.log(letsCID); - console.log(pk); - const event = { - id: letsCID, - relays: [config.relays.write], - author: pk, - kind: 1, - }; - const encodedNEvent = nostr.nip19.neventEncode(event); - xmpp.send(from, "nostr:" + encodedNEvent); - console.log(encodedNEvent) - debug(); - } - } - }); - // xmpp seems to have an error, so we show it in the console - xmpp.on('error', function (err) { - console.error(err); - }); - // set xmpp presence to nostrsms url (free advertisement! :P) - xmpp.setPresence('chat', 'https://nostrsms.com'); - // connect to xmpp server using info from config.ini - xmpp.connect({ - jid: config.xmpp.jid, + // prepare xmpp client + const xmpp = client({ + service: config.xmpp.host, + domain: config.xmpp.domain, + resource: config.xmpp.resource || 'bot', + username: config.xmpp.user, password: config.xmpp.password, - host: config.xmpp.host, - port: 5222 }); - // catch ctrl+c - // TODO: add confirmation ?? + + // set up allow users list from config.ini + const authorizedUsers = config.authorized_users.users + ? config.authorized_users.users.split(',').map(user => user.trim()) + : []; + + // we've got a connection to XMPP! + xmpp.on('online', async () => { + console.log('Connected with JID: ' + config.xmpp.user); + console.log('Pubkey: ' + pk); + await xmpp.send(xml('presence', {}, xml('status', {}, 'https://nostrsms.com'))); + }); + + // Debug: Log all incoming stanzas + xmpp.on('stanza', async (stanza) => { + console.log('Received stanza:', stanza.toString()); + + // check for chat messages + if (stanza.is('message') && stanza.attrs.type === 'chat') { + // store from jid, and message contents + const from = stanza.attrs.from; + const body = stanza.getChildText('body'); + + // show them to the user in console + console.log(`Message from ${from}: ${body}`); + + // if there is no body, it's not a message. + // it just qqs + if (!body) return; + + // deny messages if they aren't a valid user + if (!authorizedUsers.includes(from)) { + await xmpp.send(xml('message', { to: from, type: 'chat' }, xml('body', {}, 'Not authorized'))); + return; + } + // user needs help + // let's send them a wiki link back. (This should be nostrfied....right?!) + if (body === '!help') { + await xmpp.send(xml('message', { to: from, type: 'chat' }, xml('body', {}, 'https://wiki.vanderwarker.family/doku.php?id=code:nostrsms:commands'))); + // fetch global posts + } else if (body === '!g') { + await xmpp.send(xml('message', { to: from, type: 'chat' }, xml('body', {}, `Here are the latest 10 posts from global on ${config.relays.read}`))); + + async function getGlobalEvents() { + // store read relays from config.ini + const relay = nostr.relayInit(config.relays.read, WebSocket); + // we've got a connection to the nostr! + relay.on('connect', () => console.log(`connected to ${relay.url}`)); + // we've not got a connection to the nostr! + relay.on('error', () => console.log(`failed to connect to ${relay.url}`)); + + // attempt connection to the nostr + await relay.connect(); + + // create a subscription to get events + let sub = relay.sub([{ kinds: [1], limit: 10 }]); + // we've got an event! + sub.on('event', event => { + // check if the event recieved is valid json + // if it is parse it + const globalEvents = typeof event === 'string' ? JSON.parse(event) : event; + // check for json data + // continue if it exists (atomic) + if (globalEvents && globalEvents.content && globalEvents.pubkey && globalEvents.created_at) { + // store parsed data from event + const content = globalEvents.content; + const author = globalEvents.pubkey; + const createdAt = globalEvents.created_at; + // print out to console + console.log('Content:', content); + console.log('Author:', author); + console.log('Created At:', createdAt); + // send parsed event data to XMPP + xmpp.send(xml('message', { to: from, type: 'chat' }, xml('body', {}, `"${content}", - ${author} @ ${createdAt} `))); + // All set! Let's disconnect from the relay...or... + // I am not sure if I will change this... + relay.close(); + } else { + // event recieved wasn't a valid nostr event + console.error('Invalid event structure or missing required properties.'); + } + }); + } + // run and print errors to console + getGlobalEvents().catch(console.error); + } else { + // user hasn't input a command, which defaults to a nostr post. + async function newPost() { + // store write relays from config.ini + const relay = nostr.relayInit(config.relays.write, WebSocket); + // we've got a connection to the nostr! + relay.on('connect', () => console.log(`connected to ${relay.url}`)); + // we've not got a connection to the nostr! + relay.on('error', () => console.log(`failed to connect to ${relay.url}`)); + + // attempt connection to the nostr + await relay.connect(); + /* + incredible, but this is a nostr event. + truth be told it doesn't even need the tags. + That's just referencing another type of + event that advertises nostrsms and shows + links to this source code. nostr is + waaayyy more than a X (Twitter) replacement + */ + + let event = { + // kind 1 is a basic text post + kind: 1, + // set users pubkey + pubkey: pk, + // this will be created using a Unix timestamp + created_at: Math.floor(Date.now() / 1000), + // some clients will show via NostrSMS much like X has/had? idk haven't been on in years :P + tags: [['client', 'NostrSMS', '31990:9be1b8315248eeb20f9d9ab2717d1750e4f27489eab1fa531d679dadd34c2f8d:1726937516']], + // set event content to the message recieved from the user. + content: body, + }; + // sign the nostr event using privkey from config.ini + const signedEvent = nostr.finishEvent(event, sk); + // store event id + letsCID = signedEvent.id; + // sends id and pubkey to function below + showSID(letsCID, pk); + // + await relay.publish(signedEvent); + relay.close(); + } + // run and print errors to console + newPost().catch(console.error); + + // hey look the function below! + + // gets id and pk from above function ;) + async function showSID(letsCID, pk) { + // print id to console + console.log(letsCID); + // print pubkey to console + console.log(pk); + // takes the event id, your write relay(s), pubkey, and kind 1 + // mashing it together into a nevent + // (makes it easier for other clients to find the event) + const event = { + id: letsCID, + relays: [config.relays.write], + author: pk, + kind: 1, + }; + + // encode event using nip-19 (nevent) + const encodedNEvent = nostr.nip19.neventEncode(event); + // send the nevent back to the user + // the can copy and paste this into most nostr + // clients and get the event + await xmpp.send(xml('message', { to: from, type: 'chat' }, xml('body', {}, 'nostr:' + encodedNEvent))); + // also print nevent to console + console.log(encodedNEvent); + } + } + } + }); + // uh-oh, XMPP error + xmpp.on('error', (err) => { + // print error to console + console.error('XMPP Error:', err); + }); + // start XMPP connectioh + async function startXMPP() { + try { + // wait for connection + await xmpp.start(); + // print success message to console + console.log('🚀 XMPP Started Successfully'); + } catch (err) { + // print success message to console + console.error('❌ Failed to Start XMPP:', err); + // kill nostrsms process to let user try again + process.exit(1); + } + } + + // attempt to connect to XMPP + startXMPP(); + + // user hit ctrl+c! process.on('SIGINT', function () { - console.log("SigInt receieved. Shutdown inevitable!") + console.log("SigInt received. Shutdown inevitable!"); + xmpp.stop(); process.exit(); }); -// error out and show install instructions if config.ini isn't found. + +// config.ini is invalid, or missing. +// print wiki link to console for further instructions } else { console.log("=_-_+_-_=_-_+_-_=_-_+_-_=_-_+_-_=_-_+_-_="); - console.error("Config file not found at ${configPath}"); + console.error(`Config file not found at ${configPath}`); console.log("=_-_+_-_=_-_+_-_=_-_+_-_=_-_+_-_=_-_+_-_="); console.log(""); console.log("Please create a config.ini");