Skip to content

Commit

Permalink
feat: upgrade bot
Browse files Browse the repository at this point in the history
  • Loading branch information
v0l committed Oct 20, 2024
1 parent b417ff2 commit eaaa7ed
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 57 deletions.
30 changes: 30 additions & 0 deletions packages/bot/README.md
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
about: "An example bot",
})
.command("!ping", h => {
h.reply("PONG!");
})
.run();
```
20 changes: 20 additions & 0 deletions packages/bot/example/simple.ts
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
about: "An example bot",
})
.command("!ping", h => {
h.reply("PONG!");
})
.run();
4 changes: 2 additions & 2 deletions packages/bot/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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": {
Expand Down
178 changes: 126 additions & 52 deletions packages/bot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
type SystemInterface,
NostrPrefix,
EventKind,
TaggedNostrEvent,
NostrSystem,
PrivateKeySigner,
UserMetadata,
} from "@snort/system";
import EventEmitter from "eventemitter3";

Expand All @@ -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;
Expand All @@ -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() {
Expand All @@ -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
*/
Expand All @@ -106,28 +108,100 @@ export class SnortBot extends EventEmitter<BotEvents> {
return this;
}

/**
* Start the bot
*/
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]);
}
} 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)
Expand Down
2 changes: 1 addition & 1 deletion packages/system/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@snort/system",
"version": "1.5.1",
"version": "1.5.2",
"description": "Snort nostr system package",
"type": "module",
"main": "dist/index.js",
Expand Down
9 changes: 9 additions & 0 deletions packages/system/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EventExt } from "./event-ext";
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
import { Nip44Encryptor } from "./impl/nip44";
import { NostrEvent, NotSignedNostrEvent } from "./nostr";
import { randomBytes } from "@noble/hashes/utils";

export type SignerSupports = "nip04" | "nip44" | string;

Expand Down Expand Up @@ -31,6 +32,14 @@ export class PrivateKeySigner implements EventSigner {
this.#publicKey = getPublicKey(this.#privateKey);
}

/**
* Generate a new private key
*/
static random() {
const k = randomBytes(32);
return new PrivateKeySigner(k);
}

get supports(): string[] {
return ["nip04", "nip44"];
}
Expand Down
43 changes: 41 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4796,9 +4796,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "@snort/bot@workspace:packages/bot"
dependencies:
"@snort/system": "npm:^1.5.1"
"@snort/system": "npm:^1.5.2"
"@types/debug": "npm:^4.1.8"
eventemitter3: "npm:^5.0.1"
ts-node: "npm:^10.9.2"
typescript: "npm:^5.2.2"
languageName: unknown
linkType: soft
Expand Down Expand Up @@ -4858,7 +4859,7 @@ __metadata:
languageName: unknown
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
resolution: "@snort/system@workspace:packages/system"
dependencies:
Expand Down Expand Up @@ -14534,6 +14535,44 @@ __metadata:
languageName: node
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":
version: 3.15.0
resolution: "tsconfig-paths@npm:3.15.0"
Expand Down

0 comments on commit eaaa7ed

Please sign in to comment.