Compare commits

...

3 Commits

Author SHA1 Message Date
lamp 451d23fce8 accountpool 2024-12-06 01:09:40 -08:00
lamp f451c42574 debug 2024-12-05 22:57:55 -08:00
lamp 35a66a1f29 multibot 2024-12-05 22:26:57 -08:00
2 changed files with 142 additions and 113 deletions
+2
View File
@@ -2,3 +2,5 @@
node_modules node_modules
.env .env
memberlist.json memberlist.json
members.json
accountpool.json
+133 -106
View File
@@ -9,42 +9,80 @@ global.fetch = function fetch(url, options) {
return _fetch(url, options); 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)));
var memberlist = []; // list of conversation ids
try {
memberlist = JSON.parse(readFileSync("memberlist.json", "utf8"));
} catch (error) {}
function saveMemberlist() {
writeFileSync("memberlist.json", JSON.stringify(memberlist));
} }
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;
}
}
var takenIdentifiers = Array.from(members.values());
for (var {identifier, password, service} of accountpool) {
if (!takenIdentifiers.includes(identifier)) break;
}
members.set(did, identifier);
saveMembers();
let bot = bots.get(did);
if (!bot) {
bot = await newbot(identifier, password, service);
bots.set(did, bot);
}
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) {
var bot = new Bot({ let bot = new Bot({
emitEvents: false, emitEvents: false,
emitChatEvents: true emitChatEvents: true,
}); service
bot.on("error", error => { });
console.error("bot error", error); 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;
}
await bot.login({ var mainbot = await newbot("groupchatbot.bsky.social", process.env.PASSWORD);
identifier: process.env.IDENTIFIER,
password: process.env.PASSWORD
});
await bot.setChatPreference(IncomingChatPreference.All);
bot.on("message", async message => {
console.debug(new Date().toISOString(), message.senderDid, message.text); async function messageHandler(message) {
console.debug('m', new Date().toISOString(), message.bot.profile.did, message.senderDid, message.text);
try { try {
var sender = await message.getSender(); var sender = await message.getSender();
var conversation = await message.getConversation(); //var conversation = await message.getConversation();
var respond = text => sendMessage({conversationId: conversation.id, text}); var respond = text => sender.sendMessage({text});
if (message.text.startsWith('/')) { if (message.text.startsWith('/')) {
let args = message.text.split(' '); let args = message.text.split(' ');
@@ -55,33 +93,52 @@ bot.on("message", async message => {
await respond("pong"); await respond("pong");
return; return;
case "list": case "list":
let memberConversations = await Promise.all(memberlist.map(convoid => bot.getConversation(convoid))); let m = members.keys();
await respond(`${memberConversations.length} members in group chat: ${memberConversations.map(c => { m = m.filter(x => bots.has(x));
let handle = c.members.find(m => m.did != bot.profile.did).handle; m = await Promise.all(m.map(async memberdid => {
if (handle != "missing.invalid") { // work around https://github.com/skyware-js/bot/issues/19 try {
handle = `@${handle}`; 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(", ");
} }
return handle; await respond(rt);
}).join(', ')}`);
return; return;
case "leave": case "leave":
broadcast(new RichText().addMention(sender.displayName || sender.handle || message.senderDid, message.senderDid).addText(" left the group chat.")).catch(console.error); broadcast({text:new RichText().addMention(sender.displayName || sender.handle || message.senderDid, message.senderDid).addText(" left the group chat.")}).catch(console.error);
memberlist = memberlist.filter(convoid => convoid != conversation.id); removeMember(message.senderDid);
saveMemberlist(); await respond(`You left the group chat. To join the group chat again, send /join to @groupchatbot.bsky.social.`);
await respond(`You left the group chat. Rejoin at any time with /join`);
return; return;
case "join": case "join":
if (memberlist.includes(conversation.id)) { if (members.has(message.senderDid) && bots.has(message.senderDid)) {
await respond(`You're already in the group chat.`); 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 { } else {
broadcast(new RichText().addMention(sender.displayName || sender.handle || message.senderDid, message.senderDid).addText(" joined the group chat.")).catch(console.error); broadcast({text:new RichText().addMention(sender.displayName || sender.handle || message.senderDid, message.senderDid).addText(" joined the group chat.")}).catch(console.error);
memberlist.push(conversation.id); let b = await addMember(message.senderDid);
saveMemberlist(); let s = await b.getProfile(message.senderDid);
await respond(`Welcome to the group chat; you are now chatting with ${memberlist.length} other people. Use /list to see them.`); 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; return;
case "invite": case "invite":
if (!memberlist.includes(conversation.id)) { 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.`); 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; return;
} }
@@ -91,9 +148,17 @@ bot.on("message", async message => {
return; return;
} }
handle = handle.replace('@',''); handle = handle.replace('@','');
let profile = await bot.getProfile(handle); 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!`}); await profile.sendMessage({text: `@${sender.handle} invited you to the group chat! Use /join to accept!`});
await broadcast(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.")); //} 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; return;
case "help": case "help":
await respond(`Commands: ${commandList}`); await respond(`Commands: ${commandList}`);
@@ -104,11 +169,20 @@ bot.on("message", async message => {
} }
} }
if (!memberlist.includes(conversation.id)) { if (!members.has(message.senderDid)) {
await respond("Use /join to join the group chat."); await respond("Use /join to join the group chat.");
return; 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)}`; var logline = `${message.sentAt.toISOString()},${message.senderDid},${sender.displayName},${sender.handle},${JSON.stringify(message.text)}`;
console.log(logline); console.log(logline);
appendFileSync(`chatlog.csv`, logline + '\n'); appendFileSync(`chatlog.csv`, logline + '\n');
@@ -139,81 +213,34 @@ bot.on("message", async message => {
}] }]
}); });
var otherconvoids = memberlist.filter(x => x != conversation.id); await broadcast({text, facets, embed: message.embed}, {resolveFacets: false}, [message.senderDid]);
var messages = otherconvoids.map(conversationId => ({
conversationId,
text,
facets,
embed: message.embed
}));
await sendMessages(messages, {resolveFacets: false});
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (conversation) sendMessage({conversationId: conversation.id, text: `${error.message}\ncause: ${error.cause.message}`}).catch(error => console.error(error)); sender.sendMessage({text: `${error.message}\ncause: ${error.cause?.message}`}).catch(error => console.error(error));
} }
}); }
async function broadcast(text) { async function broadcast(message, options, excludeDids) {
var logline = `${new Date().toISOString()},,,,${JSON.stringify(text.text||text)}`; var logline = `${new Date().toISOString()},,,,${JSON.stringify(message.text.text||message.text)}`;
console.log(logline); console.log(logline);
appendFileSync(`chatlog.csv`, logline + '\n'); appendFileSync(`chatlog.csv`, logline + '\n');
var messages = memberlist.map(conversationId => ({ for (let [memberdid, bot] of bots) {
conversationId, if (excludeDids?.includes(memberdid)) continue;
text if (!members.has(memberdid)) continue;
})); trySendMessageToDid(bot, memberdid, message, options);
await sendMessages(messages);
}
// presere order
var queue = [];
var ratelimited = false;
function sendMessages(messages, options) {
queue.push([messages, options]);
if (!ratelimited) return flushQueue();
}
async function flushQueue() {
if (!queue.length) return;
var [messages, options] = queue.shift();
try {
await bot.sendMessages(messages, options);
ratelimited = false;
} catch (error) {
if (error.cause?.kind == "RateLimitExceeded") {
console.warn("ratelimited");
ratelimited = true;
queue.unshift([messages, options]);
} else throw error;
} }
} }
(function flushQueueLoop() { async function trySendMessageToDid(bot, did, message, options) {
flushQueue().catch(console.error).then(() => {
setTimeout(flushQueueLoop, 10000);
});
})();
// no preserve order
async function sendMessage(message, options) {
try { try {
return await bot.sendMessage(message, options); let profile = await bot.getProfile(did);
await profile.sendMessage(message, options);
} catch (error) { } catch (error) {
if (error.cause?.kind == "RateLimitExceeded") { console.error(`@ ${did} ${error.message} ${error.cause?.message}`);
console.warn("ratelimited");
await new Promise(r => setTimeout(r, 10000));
return await sendMessage(message, options);
} else throw error;
} }
} }
global.bot = bot;