271 lines
8.7 KiB
JavaScript
271 lines
8.7 KiB
JavaScript
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 <handle>`);
|
|
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); |