feat: upgrade bot
This commit is contained in:
parent
b417ff27d7
commit
eaaa7edc78
30
packages/bot/README.md
Normal file
30
packages/bot/README.md
Normal 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();
|
||||||
|
```
|
20
packages/bot/example/simple.ts
Normal file
20
packages/bot/example/simple.ts
Normal 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();
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@snort/bot",
|
"name": "@snort/bot",
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"description": "Simple bot framework",
|
"description": "Simple bot framework",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
@ -13,7 +13,7 @@
|
|||||||
"build": "rm -rf dist && tsc"
|
"build": "rm -rf dist && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@snort/system": "^1.5.1",
|
"@snort/system": "^1.5.2",
|
||||||
"eventemitter3": "^5.0.1"
|
"eventemitter3": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -6,6 +6,10 @@ import {
|
|||||||
type SystemInterface,
|
type SystemInterface,
|
||||||
NostrPrefix,
|
NostrPrefix,
|
||||||
EventKind,
|
EventKind,
|
||||||
|
TaggedNostrEvent,
|
||||||
|
NostrSystem,
|
||||||
|
PrivateKeySigner,
|
||||||
|
UserMetadata,
|
||||||
} from "@snort/system";
|
} from "@snort/system";
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
|
|
||||||
@ -15,11 +19,26 @@ export interface BotEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BotMessage {
|
export interface BotMessage {
|
||||||
|
/**
|
||||||
|
* Event which this message belongs to
|
||||||
|
*/
|
||||||
link: NostrLink;
|
link: NostrLink;
|
||||||
|
/**
|
||||||
|
* Pubkey of the message author
|
||||||
|
*/
|
||||||
from: string;
|
from: string;
|
||||||
|
/**
|
||||||
|
* Message content string
|
||||||
|
*/
|
||||||
message: string;
|
message: string;
|
||||||
|
/**
|
||||||
|
* Original message event
|
||||||
|
*/
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
reply: (msg: string) => void;
|
/**
|
||||||
|
* Reply handler for this message
|
||||||
|
*/
|
||||||
|
reply: (msg: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommandHandler = (msg: BotMessage) => void;
|
export type CommandHandler = (msg: BotMessage) => void;
|
||||||
@ -35,48 +54,15 @@ export class SnortBot extends EventEmitter<BotEvents> {
|
|||||||
readonly publisher: EventPublisher,
|
readonly publisher: EventPublisher,
|
||||||
) {
|
) {
|
||||||
super();
|
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));
|
* Create a new simple bot
|
||||||
const linkStr = links.map(e => e.encode());
|
*/
|
||||||
if (linkStr.every(a => this.#activeStreamSub.has(a))) {
|
static simple(name: string) {
|
||||||
return;
|
const system = new NostrSystem({});
|
||||||
}
|
const signer = PrivateKeySigner.random();
|
||||||
const rb = new RequestBuilder("stream-chat");
|
return new SnortBot(name, system, new EventPublisher(signer, signer.getPubKey()));
|
||||||
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() {
|
get activeStreams() {
|
||||||
@ -94,6 +80,22 @@ export class SnortBot extends EventEmitter<BotEvents> {
|
|||||||
return this;
|
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
|
* Simple command handler
|
||||||
*/
|
*/
|
||||||
@ -106,6 +108,9 @@ export class SnortBot extends EventEmitter<BotEvents> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the bot
|
||||||
|
*/
|
||||||
run() {
|
run() {
|
||||||
const req = new RequestBuilder("streams");
|
const req = new RequestBuilder("streams");
|
||||||
req.withOptions({ leaveOpen: true });
|
req.withOptions({ leaveOpen: true });
|
||||||
@ -113,21 +118,90 @@ export class SnortBot extends EventEmitter<BotEvents> {
|
|||||||
if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) {
|
if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) {
|
||||||
req.withFilter().authors([link.id]).kinds([30311]);
|
req.withFilter().authors([link.id]).kinds([30311]);
|
||||||
req.withFilter().tag("p", [link.id]).kinds([30311]);
|
req.withFilter().tag("p", [link.id]).kinds([30311]);
|
||||||
} else if (link.type === NostrPrefix.Address) {
|
} else {
|
||||||
const f = req.withFilter().tag("d", [link.id]);
|
req.withFilter().link(link);
|
||||||
if (link.author) {
|
|
||||||
f.authors([link.author]);
|
|
||||||
}
|
|
||||||
if (link.kind) {
|
|
||||||
f.kinds([link.kind]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
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) {
|
async #sendReplyTo(link: NostrLink, msg: string) {
|
||||||
const ev = await this.publisher.generic(eb => {
|
const ev = await this.publisher.generic(eb => {
|
||||||
eb.kind(1311 as EventKind)
|
eb.kind(1311 as EventKind)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@snort/system",
|
"name": "@snort/system",
|
||||||
"version": "1.5.1",
|
"version": "1.5.2",
|
||||||
"description": "Snort nostr system package",
|
"description": "Snort nostr system package",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
@ -4,6 +4,7 @@ import { EventExt } from "./event-ext";
|
|||||||
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
|
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
|
||||||
import { Nip44Encryptor } from "./impl/nip44";
|
import { Nip44Encryptor } from "./impl/nip44";
|
||||||
import { NostrEvent, NotSignedNostrEvent } from "./nostr";
|
import { NostrEvent, NotSignedNostrEvent } from "./nostr";
|
||||||
|
import { randomBytes } from "@noble/hashes/utils";
|
||||||
|
|
||||||
export type SignerSupports = "nip04" | "nip44" | string;
|
export type SignerSupports = "nip04" | "nip44" | string;
|
||||||
|
|
||||||
@ -31,6 +32,14 @@ export class PrivateKeySigner implements EventSigner {
|
|||||||
this.#publicKey = getPublicKey(this.#privateKey);
|
this.#publicKey = getPublicKey(this.#privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new private key
|
||||||
|
*/
|
||||||
|
static random() {
|
||||||
|
const k = randomBytes(32);
|
||||||
|
return new PrivateKeySigner(k);
|
||||||
|
}
|
||||||
|
|
||||||
get supports(): string[] {
|
get supports(): string[] {
|
||||||
return ["nip04", "nip44"];
|
return ["nip04", "nip44"];
|
||||||
}
|
}
|
||||||
|
43
yarn.lock
43
yarn.lock
@ -4796,9 +4796,10 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@snort/bot@workspace:packages/bot"
|
resolution: "@snort/bot@workspace:packages/bot"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@snort/system": "npm:^1.5.1"
|
"@snort/system": "npm:^1.5.2"
|
||||||
"@types/debug": "npm:^4.1.8"
|
"@types/debug": "npm:^4.1.8"
|
||||||
eventemitter3: "npm:^5.0.1"
|
eventemitter3: "npm:^5.0.1"
|
||||||
|
ts-node: "npm:^10.9.2"
|
||||||
typescript: "npm:^5.2.2"
|
typescript: "npm:^5.2.2"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@ -4858,7 +4859,7 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@snort/system@npm:^1.0.21, @snort/system@npm:^1.2.11, @snort/system@npm:^1.5.1, @snort/system@workspace:*, @snort/system@workspace:packages/system":
|
"@snort/system@npm:^1.0.21, @snort/system@npm:^1.2.11, @snort/system@npm:^1.5.1, @snort/system@npm:^1.5.2, @snort/system@workspace:*, @snort/system@workspace:packages/system":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@snort/system@workspace:packages/system"
|
resolution: "@snort/system@workspace:packages/system"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -14534,6 +14535,44 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ts-node@npm:^10.9.2":
|
||||||
|
version: 10.9.2
|
||||||
|
resolution: "ts-node@npm:10.9.2"
|
||||||
|
dependencies:
|
||||||
|
"@cspotcode/source-map-support": "npm:^0.8.0"
|
||||||
|
"@tsconfig/node10": "npm:^1.0.7"
|
||||||
|
"@tsconfig/node12": "npm:^1.0.7"
|
||||||
|
"@tsconfig/node14": "npm:^1.0.0"
|
||||||
|
"@tsconfig/node16": "npm:^1.0.2"
|
||||||
|
acorn: "npm:^8.4.1"
|
||||||
|
acorn-walk: "npm:^8.1.1"
|
||||||
|
arg: "npm:^4.1.0"
|
||||||
|
create-require: "npm:^1.1.0"
|
||||||
|
diff: "npm:^4.0.1"
|
||||||
|
make-error: "npm:^1.1.1"
|
||||||
|
v8-compile-cache-lib: "npm:^3.0.1"
|
||||||
|
yn: "npm:3.1.1"
|
||||||
|
peerDependencies:
|
||||||
|
"@swc/core": ">=1.2.50"
|
||||||
|
"@swc/wasm": ">=1.2.50"
|
||||||
|
"@types/node": "*"
|
||||||
|
typescript: ">=2.7"
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@swc/core":
|
||||||
|
optional: true
|
||||||
|
"@swc/wasm":
|
||||||
|
optional: true
|
||||||
|
bin:
|
||||||
|
ts-node: dist/bin.js
|
||||||
|
ts-node-cwd: dist/bin-cwd.js
|
||||||
|
ts-node-esm: dist/bin-esm.js
|
||||||
|
ts-node-script: dist/bin-script.js
|
||||||
|
ts-node-transpile-only: dist/bin-transpile.js
|
||||||
|
ts-script: dist/bin-script-deprecated.js
|
||||||
|
checksum: 10/a91a15b3c9f76ac462f006fa88b6bfa528130dcfb849dd7ef7f9d640832ab681e235b8a2bc58ecde42f72851cc1d5d4e22c901b0c11aa51001ea1d395074b794
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"tsconfig-paths@npm:^3.15.0":
|
"tsconfig-paths@npm:^3.15.0":
|
||||||
version: 3.15.0
|
version: 3.15.0
|
||||||
resolution: "tsconfig-paths@npm:3.15.0"
|
resolution: "tsconfig-paths@npm:3.15.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user