258 lines
9.5 KiB
JavaScript

// 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 || 'nostrsms-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 rawfrom = stanza.attrs.from;
const from = rawfrom.split('/')[0];
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);
}