feat: upgrade bot

This commit is contained in:
2024-10-20 22:11:34 +01:00
parent b417ff27d7
commit eaaa7edc78
7 changed files with 229 additions and 57 deletions

30
packages/bot/README.md Normal file
View File

@ -0,0 +1,30 @@
# @snort/bot
Simple live stream event chat bot (NIP-53)
## Example
```typescript
import { parseNostrLink } from "@snort/system";
import { SnortBot } from "../src/index";
// listen to chat events on every NoGood live stream
const noGoodLink = parseNostrLink("npub12hcytyr8fumy3axde8wgeced523gyp6v6zczqktwuqeaztfc2xzsz3rdp4");
// Run a simple bot
SnortBot.simple("example")
.link(noGoodLink)
.relay("wss://relay.damus.io")
.relay("wss://nos.lol")
.relay("wss://relay.nostr.band")
.profile({
name: "PingBot",
picture: "https://nostr.download/572f5ff8286e8c719196f904fed24aef14586ec8181c14b09efa726682ef48ef",
lud16: "kieran@zap.stream",
about: "An example bot",
})
.command("!ping", h => {
h.reply("PONG!");
})
.run();
```

View File

@ -0,0 +1,20 @@
import { parseNostrLink } from "@snort/system";
import { SnortBot } from "../src/index";
const noGoodLink = parseNostrLink("npub12hcytyr8fumy3axde8wgeced523gyp6v6zczqktwuqeaztfc2xzsz3rdp4");
SnortBot.simple("example")
.link(noGoodLink)
.relay("wss://relay.damus.io")
.relay("wss://nos.lol")
.relay("wss://relay.nostr.band")
.profile({
name: "PingBot",
picture: "https://nostr.download/572f5ff8286e8c719196f904fed24aef14586ec8181c14b09efa726682ef48ef",
lud16: "kieran@zap.stream",
about: "An example bot",
})
.command("!ping", h => {
h.reply("PONG!");
})
.run();

View File

@ -1,6 +1,6 @@
{
"name": "@snort/bot",
"version": "1.1.1",
"version": "1.2.0",
"description": "Simple bot framework",
"type": "module",
"module": "src/index.ts",
@ -13,7 +13,7 @@
"build": "rm -rf dist && tsc"
},
"dependencies": {
"@snort/system": "^1.5.1",
"@snort/system": "^1.5.2",
"eventemitter3": "^5.0.1"
},
"devDependencies": {

View File

@ -6,6 +6,10 @@ import {
type SystemInterface,
NostrPrefix,
EventKind,
TaggedNostrEvent,
NostrSystem,
PrivateKeySigner,
UserMetadata,
} from "@snort/system";
import EventEmitter from "eventemitter3";
@ -15,11 +19,26 @@ export interface BotEvents {
}
export interface BotMessage {
/**
* Event which this message belongs to
*/
link: NostrLink;
/**
* Pubkey of the message author
*/
from: string;
/**
* Message content string
*/
message: string;
/**
* Original message event
*/
event: NostrEvent;
reply: (msg: string) => void;
/**
* Reply handler for this message
*/
reply: (msg: string) => Promise<void>;
}
export type CommandHandler = (msg: BotMessage) => void;
@ -35,48 +54,15 @@ export class SnortBot extends EventEmitter<BotEvents> {
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);
},
});
}
}
});
}
/**
* Create a new simple bot
*/
static simple(name: string) {
const system = new NostrSystem({});
const signer = PrivateKeySigner.random();
return new SnortBot(name, system, new EventPublisher(signer, signer.getPubKey()));
}
get activeStreams() {
@ -94,6 +80,22 @@ export class SnortBot extends EventEmitter<BotEvents> {
return this;
}
/**
* Add a relay for communication
*/
relay(r: string) {
this.system.ConnectToRelay(r, { read: true, write: true });
return this;
}
/**
* Create a profile
*/
profile(p: UserMetadata) {
this.publisher.metadata(p).then(ev => this.system.BroadcastEvent(ev));
return this;
}
/**
* Simple command handler
*/
@ -106,6 +108,9 @@ export class SnortBot extends EventEmitter<BotEvents> {
return this;
}
/**
* Start the bot
*/
run() {
const req = new RequestBuilder("streams");
req.withOptions({ leaveOpen: true });
@ -113,21 +118,90 @@ export class SnortBot extends EventEmitter<BotEvents> {
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]);
}
} else {
req.withFilter().link(link);
}
}
this.system.Query(req);
// requst streams by input links
const q = this.system.Query(req);
q.on("event", evs => {
for (const e of evs) {
this.#handleEvent(e);
}
});
// setup chat query, its empty for now
const rbChat = new RequestBuilder("stream-chat");
rbChat.withOptions({ replaceable: true, leaveOpen: true });
const qChat = this.system.Query(rbChat);
qChat.on("event", evs => {
for (const e of evs) {
this.#handleEvent(e);
}
});
return this;
}
/**
* Send a message to all active streams
*/
async notify(msg: string) {
for (const stream of this.activeStreams) {
const ev = await this.publisher.reply(stream, msg, eb => {
return eb.kind(1311 as EventKind);
});
await this.system.BroadcastEvent(ev);
}
}
#handleEvent(e: TaggedNostrEvent) {
this.emit("event", e);
if (e.kind === 30311) {
this.#checkActiveStreams(e);
} 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),
});
}
}
}
#checkActiveStreams(e: TaggedNostrEvent) {
const links = [e, ...this.activeStreams].map(v => NostrLink.fromEvent(v));
const linkStr = [...new Set(links.map(e => e.encode()))];
if (linkStr.every(a => this.#activeStreamSub.has(a))) {
return;
}
const rb = new RequestBuilder("stream-chat");
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);
}
async #sendReplyTo(link: NostrLink, msg: string) {
const ev = await this.publisher.generic(eb => {
eb.kind(1311 as EventKind)