// initialize required packages const fs = require('fs'); const ini = require('ini'); const nostr = require('nostr-tools'); const os = require('os'); const path = require('path'); const wspf = require('websocket-polyfill'); const WebSocket = require('ws'); const { client, xml } = require('@xmpp/client'); // nostr things let { bech32, bech32m } = require('bech32'); let letsCID; let signedEvent; // config setup for home folders // Determine the platform-specific config path function getConfigPath() { const platform = os.platform(); if (platform === 'win32') { // Windows return path.join(process.env.APPDATA, 'nostrsms', 'config.ini'); } else if (platform === 'darwin') { // macOS return path.join(os.homedir(), 'Library', 'Application Support', 'nostrsms', 'config.ini'); } else { // Linux and other Unix-like systems return path.join(os.homedir(), '.config', 'nostrsms', 'config.ini'); } } // prepare config for parsing const configPath = getConfigPath(); if (fs.existsSync(configPath)) { console.log(`Found config at ${configPath}`); const config = ini.parse(fs.readFileSync(configPath, 'utf-8')); let sk = config.nostr.pkhex; let pk = nostr.getPublicKey(sk); // 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, }); // 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 received. Shutdown inevitable!"); xmpp.stop(); process.exit(); }); // 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.log("=_-_+_-_=_-_+_-_=_-_+_-_=_-_+_-_=_-_+_-_="); console.log(""); console.log("Please create a config.ini"); console.log("See https://wiki.vanderwarker.family/doku.php?id=code:nostrsms:install"); console.log("and choose your preferred install method."); process.exit(1); }