import 'dotenv/config'; import {Bot, IncomingChatPreference, RichText} from "@skyware/bot"; import fetchRetry from "fetch-retry"; import serveHandler from 'serve-handler'; import { createServer } from "http"; import { appendFileSync, readFileSync, writeFileSync } from "fs"; var _fetch = fetchRetry(global.fetch); global.fetch = function fetch(url, options) { //console.debug("fetch", String(url)); return _fetch(url, options); } var accountpool = JSON.parse(readFileSync("accountpool.json", "utf8")); var members = new Map(JSON.parse(readFileSync("members.json", "utf8"))); // member did => bot identifier function saveMembers() { writeFileSync("members.json", JSON.stringify(Array.from(members))); } async function addMember(did) { if (members.has(did)) { if (bots.has(did)) { return false; } else { let identifier = members.get(did); let {password, service} = accountpool.find(x => x.identifier == identifier); let bot = newbot(identifier, password, service); bots.set(did, bot); return bot; } } let bot = bots.get(did); if (bot) { var identifier = bot.profile.handle; } else { var takenIdentifiers = new Set([...members.values(), ...Array.from(bots.values()).map(bot => bot.profile.handle)]); for (var {identifier, password, service} of accountpool) { if (!takenIdentifiers.has(identifier)) break; } bot = await newbot(identifier, password, service); bots.set(did, bot); } members.set(did, identifier); saveMembers(); return bot; } function removeMember(did) { members.delete(did); saveMembers(); } var bots = new Map(await Promise.all(Array.from(members).map(async ([memberdid, identifier]) => { let {password, service} = accountpool.find(x => /*x.did == identifier ||*/ x.identifier == identifier); let bot = await newbot(identifier, password, service); return [memberdid, bot]; }))); // member did => Bot instance async function newbot(identifier, password, service) { let bot = new Bot({ emitEvents: false, emitChatEvents: true, service }); bot.on("error", error => { console.error(`bot ${identifier} error`, error); }); bot.on("message", messageHandler); await bot.login({ identifier, password }); console.log(`logged in bot ${identifier}`); await bot.setChatPreference(IncomingChatPreference.All); return bot; } var mainbot = await newbot("groupchatbot.bsky.social", process.env.PASSWORD); async function messageHandler(message) { console.debug('m', new Date().toISOString(), message.bot.profile.did, message.senderDid, message.text); try { var sender = await message.getSender(); //var conversation = await message.getConversation(); var respond = text => sender.sendMessage({text}); if (message.text.startsWith('/')) { let args = message.text.split(' '); let cmd = args[0].slice(1).toLowerCase(); let commandList = `/list, /leave, /join, /invite, /ping`; switch(cmd) { case "ping": await respond("pong"); return; case "list": let m = members.keys(); m = m.filter(x => bots.has(x)); m = await Promise.all(m.map(async memberdid => { try { return await message.bot.getProfile(memberdid); } catch(error) {} return memberdid; })); let rt = new RichText(); rt.addText(`${m.length} members in group chat: `); for (let p of m) { rt.addMention(p.displayName || p.handle || p.did || p, p.did || p); rt.addText(", "); } await respond(rt); return; case "leave": broadcast({text:new RichText().addMention(sender.displayName || sender.handle || message.senderDid, message.senderDid).addText(" left the group chat.")}).catch(console.error); removeMember(message.senderDid); await respond(`You left the group chat. To join the group chat again, send /join to @groupchatbot.bsky.social.`); return; case "join": if (members.has(message.senderDid) && bots.has(message.senderDid)) { await (await bots.get(message.senderDid).getProfile(message.senderDid)).sendMessage({ text: `Use this bot to access group chat.` }); await respond(`You are already in the group chat, use @${bots.get(message.senderDid).profile.handle} to access it.`); return; } else { broadcast({text:new RichText().addMention(sender.displayName || sender.handle || message.senderDid, message.senderDid).addText(" joined the group chat.")}).catch(console.error); let b = await addMember(message.senderDid); let s = await b.getProfile(message.senderDid); await respond(`You shall receive a message from @${b.profile.handle}, use that bot to access the group chat.`); try { await s.sendMessage({ text: `Welcome to the group chat; you are now chatting with ${members.size} other people. Use /list to see them.` }); } catch (error) { console.error(error); await respond(`The other groupchat bot failed to DM you. Please allow DMs from everyone or open a DM with @${b.profile.handle} to access the group chat.\n\n(${error.message}\n${error.cause})`); } } return; case "invite": if (!members.has(message.senderDid)) { await respond(`You are not allowed to invite people to the group chat when you are not in the group chat yourself. Use /join first.`); return; } let handle = args[1]; if (!handle) { await respond(`Usage: /invite `); return; } handle = handle.replace('@',''); let profile = await mainbot.getProfile(handle); if (members.has(profile.did)) { await respond("They're already here!"); return; } //try { await profile.sendMessage({text: `@${sender.handle} invited you to the group chat! Use /join to accept!`}); //} catch (error) { //} await broadcast({text: new RichText().addMention(sender.displayName || sender.handle || message.senderDid, message.senderDid).addText(" invited ").addMention(profile.displayName||profile.handle,profile.did).addText(" to the group chat.")}); return; case "help": await respond(`Commands: ${commandList}`); return; default: await respond(`Only these commands are accepted: ${commandList}`); return; } } if (!members.has(message.senderDid)) { await respond("Use /join to join the group chat."); return; } if (!bots.has(message.senderDid)) { await addMember(message.senderDid); } if (bots.get(message.senderDid) != message.bot) { await respond(`Wrong bot. Use @${bots.get(message.senderDid).profile?.handle} to access the group chat.`); return; } var logline = `${message.sentAt.toISOString()},${message.senderDid},${sender.displayName},${sender.handle},${JSON.stringify(message.text)}`; console.log(logline); appendFileSync(`chatlog.csv`, logline + '\n'); var text = message.text; var facets = message.facets || []; var linkedName = sender.displayName || sender.handle; var prefix = `${linkedName}: `; var prefixByteLength = new Blob([prefix]).size; facets = facets.map(f => { var facet = f.toRecord(); facet.index.byteStart += prefixByteLength; facet.index.byteEnd += prefixByteLength; return facet; }); text = prefix + text; facets.unshift({ index: { byteStart: 0, byteEnd: new Blob([linkedName]).size }, features: [{ did: sender.did, "$type": "app.bsky.richtext.facet#mention" }] }); await broadcast({text, facets, embed: message.embed}, {resolveFacets: false}, [message.senderDid]); } catch (error) { console.error(error); sender.sendMessage({text: `${error.message}\ncause: ${error.cause?.message}`}).catch(error => console.error(error)); } } async function broadcast(message, options, excludeDids) { if (!excludeDids) { var logline = `${new Date().toISOString()},,,,${JSON.stringify(message.text.text||message.text)}`; console.log(logline); appendFileSync(`chatlog.csv`, logline + '\n'); } for (let [memberdid, bot] of bots) { if (excludeDids?.includes(memberdid)) continue; if (!members.has(memberdid)) continue; trySendMessageToDid(bot, memberdid, message, options); } } async function trySendMessageToDid(bot, did, message, options) { try { let profile = await bot.getProfile(did); await profile.sendMessage(message, options); } catch (error) { console.error(`@ ${did} ${error.message} ${error.cause?.message}`); } } createServer((req, res) => { serveHandler(req, res, { public: "public", headers: [{ source: "*.csv", headers: [{ key: "Content-Type", value: "text/plain; charset=utf-8" }] }] }); }).listen(2383);