From ed8aec1008c8b0e793e39bca65d91a0b67c49407 Mon Sep 17 00:00:00 2001 From: kieran Date: Sun, 20 Oct 2024 20:03:20 +0100 Subject: [PATCH] feat: @snort/bot --- packages/bot/package.json | 23 +++++++ packages/bot/src/index.ts | 137 +++++++++++++++++++++++++++++++++++++ packages/bot/tsconfig.json | 18 +++++ packages/bot/typedoc.json | 3 + yarn.lock | 11 +++ 5 files changed, 192 insertions(+) create mode 100644 packages/bot/package.json create mode 100644 packages/bot/src/index.ts create mode 100644 packages/bot/tsconfig.json create mode 100644 packages/bot/typedoc.json diff --git a/packages/bot/package.json b/packages/bot/package.json new file mode 100644 index 00000000..c15aedce --- /dev/null +++ b/packages/bot/package.json @@ -0,0 +1,23 @@ +{ + "name": "@snort/bot", + "version": "1.1.1", + "description": "Simple bot framework", + "type": "module", + "module": "src/index.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "repository": "https://git.v0l.io/Kieran/snort", + "author": "Kieran", + "license": "MIT", + "scripts": { + "build": "rm -rf dist && tsc" + }, + "dependencies": { + "@snort/system": "^1.5.1", + "eventemitter3": "^5.0.1" + }, + "devDependencies": { + "@types/debug": "^4.1.8", + "typescript": "^5.2.2" + } +} diff --git a/packages/bot/src/index.ts b/packages/bot/src/index.ts new file mode 100644 index 00000000..bf15db3d --- /dev/null +++ b/packages/bot/src/index.ts @@ -0,0 +1,137 @@ +import { + EventPublisher, + NostrLink, + RequestBuilder, + type NostrEvent, + type SystemInterface, + NostrPrefix, + EventKind +} from "@snort/system"; +import EventEmitter from "eventemitter3"; + +export interface BotEvents { + message: (msg: BotMessage) => void, + event: (ev: NostrEvent) => void, +} + +export interface BotMessage { + link: NostrLink, + from: string, + message: string, + event: NostrEvent, + reply: (msg: string) => void, +} + +export type CommandHandler = (msg: BotMessage) => void; + +export class SnortBot extends EventEmitter { + #streams: Array = []; + #seen: Set = new Set(); + #activeStreamSub: Set = new Set(); + + constructor( + readonly name: string, + readonly system: SystemInterface, + readonly publisher: EventPublisher + ) { + super(); + system.pool.on("event", (addr, sub, e) => { + this.emit("event", e); + if (e.kind === 30311) { + const links = [e, ...this.activeStreams].map(v => NostrLink.fromEvent(v)) + const linkStr = links.map(e => e.encode()); + if (linkStr.every(a => this.#activeStreamSub.has(a))) { + return; + } + const rb = new RequestBuilder("stream-chat"); + rb.withOptions({ replaceable: true, leaveOpen: true }); + rb.withFilter() + .kinds([1311 as EventKind]) + .replyToLink(links) + .since(Math.floor(new Date().getTime() / 1000)); + this.system.Query(rb); + console.log("Looking for chat messages from: ", linkStr); + this.#activeStreamSub = new Set(linkStr); + } else if (e.kind === 1311) { + // skip my own messages + if (e.pubkey === this.publisher.pubKey) { + return; + } + // skip already seen chat messages + if (this.#seen.has(e.id)) { + return; + } + this.#seen.add(e.id); + const streamTag = e.tags.find(a => a[0] === "a" && a[1].startsWith("30311:")); + if (streamTag) { + const link = NostrLink.fromTag(streamTag); + this.emit("message", { + link, + from: e.pubkey, + message: e.content, + event: e, + reply: (msg: string) => { + this.#sendReplyTo(link, msg); + } + }); + } + } + }); + } + + get activeStreams() { + return this.system.GetQuery("streams")?.snapshot?.filter(a => a.tags.find(b => b[0] === "status")?.at(1) === "live") ?? [] + } + + /** + * Add a stream to listen on + */ + link(a: NostrLink) { + this.#streams.push(a); + return this; + } + + /** + * Simple command handler + */ + command(cmd: string, h: CommandHandler) { + this.on("message", m => { + if (m.message.startsWith(cmd)) { + h(m); + } + }); + return this; + } + + run() { + const req = new RequestBuilder("streams"); + req.withOptions({ leaveOpen: true }); + for (const link of this.#streams) { + if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) { + req.withFilter().authors([link.id]).kinds([30311]); + req.withFilter().tag("p", [link.id]).kinds([30311]); + } else if (link.type === NostrPrefix.Address) { + const f = req.withFilter().tag("d", [link.id]); + if (link.author) { + f.authors([link.author]); + } + if (link.kind) { + f.kinds([link.kind]); + } + } + } + + this.system.Query(req); + return this; + } + + async #sendReplyTo(link: NostrLink, msg: string) { + const ev = await this.publisher.generic(eb => { + eb.kind(1311 as EventKind) + .tag(link.toEventTag("root")!) + .content(msg); + return eb; + }); + await this.system.BroadcastEvent(ev); + } +} \ No newline at end of file diff --git a/packages/bot/tsconfig.json b/packages/bot/tsconfig.json new file mode 100644 index 00000000..40417eff --- /dev/null +++ b/packages/bot/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "noImplicitOverride": true, + "module": "ESNext", + "strict": true, + "declaration": true, + "declarationMap": true, + "inlineSourceMap": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["./src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/bot/typedoc.json b/packages/bot/typedoc.json new file mode 100644 index 00000000..35fed2c9 --- /dev/null +++ b/packages/bot/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index 1af22cc2..873e6028 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4792,6 +4792,17 @@ __metadata: languageName: unknown linkType: soft +"@snort/bot@workspace:packages/bot": + version: 0.0.0-use.local + resolution: "@snort/bot@workspace:packages/bot" + dependencies: + "@snort/system": "npm:^1.5.1" + "@types/debug": "npm:^4.1.8" + eventemitter3: "npm:^5.0.1" + typescript: "npm:^5.2.2" + languageName: unknown + linkType: soft + "@snort/shared@npm:^1.0.14, @snort/shared@npm:^1.0.17, @snort/shared@npm:^1.0.6, @snort/shared@workspace:*, @snort/shared@workspace:packages/shared": version: 0.0.0-use.local resolution: "@snort/shared@workspace:packages/shared"