Compare commits

...

17 Commits

Author SHA1 Message Date
lamp ffc634a110 fix 2 bug 2024-12-07 00:19:38 -08:00
lamp e36208c1f5 bug 2024-12-06 02:11:09 -08:00
lamp 36f7b47dc6 server for logs 2024-12-06 02:05:35 -08:00
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
lamp c48f109d15 message queue 2024-12-04 23:44:08 -08:00
lamp d71e509e0a /invite 2024-12-04 22:35:36 -08:00
lamp c983e2ac47 use own member list, join/leave msgs 2024-12-04 22:09:19 -08:00
lamp 43df6863e4 escape csv value 2024-11-30 23:32:15 -08:00
lamp fda682c302 support embed 2024-11-30 22:48:30 -08:00
lamp cd026cbd8c handle ratelimit 2024-11-24 01:22:13 -08:00
lamp 663cd505be work around facet bug 2024-11-24 00:33:38 -08:00
lamp a5c23411ec handle blocking 2024-11-22 22:56:49 -08:00
lamp da34c10121 fetch-retry 2024-11-04 13:41:26 -08:00
lamp bf88195901 catch invalid command 2024-11-02 16:14:02 -07:00
lamp c9d312d78f handle bot error 2024-11-02 16:06:37 -07:00
4 changed files with 334 additions and 51 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
chatlog.csv
*.csv
node_modules
.env
memberlist.json
members.json
accountpool.json
+216 -42
View File
@@ -1,49 +1,193 @@
import 'dotenv/config';
import {Bot, IncomingChatPreference, RichText} from "@skyware/bot";
import { appendFileSync } from "fs";
import fetchRetry from "fetch-retry";
import serveHandler from 'serve-handler';
import { createServer } from "http";
import { appendFileSync, readFileSync, writeFileSync } from "fs";
var bot = new Bot({
emitEvents: false,
emitChatEvents: true
});
await bot.login({
identifier: process.env.IDENTIFIER,
password: process.env.PASSWORD
});
await bot.setChatPreference(IncomingChatPreference.All);
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
bot.on("message", async message => {
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();
if (!conversation) { console.error("missing conversation"); }
//var conversation = await message.getConversation();
var respond = text => sender.sendMessage({text});
if (message.text.startsWith('/')) {
let respond = text => conversation.sendMessage({text});
let cmd = message.text.split(' ')[0].slice(1).toLowerCase();
let args = message.text.split(' ');
let cmd = args[0].slice(1).toLowerCase();
let commandList = `/list, /leave, /join, /invite, /ping`;
switch(cmd) {
case "list":
let {conversations} = await bot.listConversations();
await respond(`${conversations.length} members in group chat: ${conversations.map(c => '@' + c.members.find(m => m.did != bot.profile.did).handle).join(', ')}`);
return;
case "leave":
await respond(`You left the group chat and will no longer receive messages! Send any message to join again, or send /join to re-join silently.`);
await conversation.leave();
return;
case "join":
await respond(`Welcome to the group chat!`);
return;
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: /list, /leave, /join, /ping`);
await respond(`Commands: ${commandList}`);
return;
default:
await respond(`Only these commands are accepted: ${commandList}`);
return;
}
}
var logline = `${message.sentAt.toISOString()},${message.senderDid},${sender.displayName},${sender.handle},${message.text}`;
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');
@@ -73,25 +217,55 @@ bot.on("message", async message => {
}]
});
var {conversations} = await bot.listConversations();
var otherConservations = conversations.filter(c => c.id != conversation?.id);
//otherConservations = otherConservations.filter(c => c.members.length > 1); // doesn't work, bsky doesnt let you know when they leave?
var messages = otherConservations.map(c => ({
conversationId: c.id,
text,
facets
}));
await bot.sendMessages(messages, {resolveFacets: false});
await broadcast({text, facets, embed: message.embed}, {resolveFacets: false}, [message.senderDid]);
} catch (error) {
console.error(error);
conversation?.sendMessage({text: error.message}).catch(error => console.error(error));
sender.sendMessage({text: `${error.message}\ncause: ${error.cause?.message}`}).catch(error => console.error(error));
}
});
}
global.bot = bot;
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);
+109 -5
View File
@@ -5,8 +5,10 @@
"packages": {
"": {
"dependencies": {
"@skyware/bot": "^0.3.7-beta.1",
"dotenv": "^16.4.5"
"@skyware/bot": "^0.3.7",
"dotenv": "^16.4.5",
"fetch-retry": "^6.0.0",
"serve-handler": "^6.1.6"
}
},
"node_modules/@atcute/base32": {
@@ -93,9 +95,9 @@
"optional": true
},
"node_modules/@skyware/bot": {
"version": "0.3.7-beta.1",
"resolved": "https://registry.npmjs.org/@skyware/bot/-/bot-0.3.7-beta.1.tgz",
"integrity": "sha512-flH5DzHKBuLfSSJsHguh3ZcsTaillUo5sq1llrXEU6xY9Btwe2B97dvlvTNdDOHJeyXse5Mlwyq5CE4aeF/xHw==",
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/@skyware/bot/-/bot-0.3.7.tgz",
"integrity": "sha512-27k7r4/YA+h8FobXokMa3iv8fm/850a7NmajOHP3/CaMbCYicXXBDnAIY4kIPP58Zl/A/JvdcFLpQBpfFkiQHQ==",
"dependencies": {
"@atcute/bluesky": "^1.0.7",
"@atcute/bluesky-richtext-builder": "^1.0.1",
@@ -132,6 +134,41 @@
"partysocket": "^1.0.2"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
@@ -156,6 +193,41 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/fetch-retry": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz",
"integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag=="
},
"node_modules/mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
"dependencies": {
"mime-db": "~1.33.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/partysocket": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.0.2.tgz",
@@ -166,6 +238,16 @@
"event-target-shim": "^6.0.2"
}
},
"node_modules/path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w=="
},
"node_modules/path-to-regexp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="
},
"node_modules/quick-lru": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.0.0.tgz",
@@ -178,6 +260,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/rate-limit-threshold": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/rate-limit-threshold/-/rate-limit-threshold-0.1.5.tgz",
@@ -187,6 +277,20 @@
"node": "^14.13.1 || >=16.0.0"
}
},
"node_modules/serve-handler": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
"dependencies": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
"mime-types": "2.1.18",
"minimatch": "3.1.2",
"path-is-inside": "1.0.2",
"path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
}
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+4 -2
View File
@@ -1,6 +1,8 @@
{
"dependencies": {
"@skyware/bot": "^0.3.7-beta.1",
"dotenv": "^16.4.5"
"@skyware/bot": "^0.3.7",
"dotenv": "^16.4.5",
"fetch-retry": "^6.0.0",
"serve-handler": "^6.1.6"
}
}