applesauce-actions 0.0.0-next-20250308144838 → 0.0.0-next-20250311125119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/fake-user.d.ts +10 -0
- package/dist/__tests__/fake-user.js +32 -0
- package/dist/action-hub.d.ts +34 -0
- package/dist/action-hub.js +47 -0
- package/dist/actions/__tests__/contacts.test.d.ts +1 -0
- package/dist/actions/__tests__/contacts.test.js +53 -0
- package/dist/actions/contacts.d.ts +7 -0
- package/dist/actions/contacts.js +35 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +16 -6
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
|
2
|
+
import type { NostrEvent } from "nostr-tools";
|
|
3
|
+
export declare class FakeUser extends SimpleSigner {
|
|
4
|
+
pubkey: string;
|
|
5
|
+
event(data?: Partial<NostrEvent>): NostrEvent;
|
|
6
|
+
note(content?: string, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
7
|
+
profile(profile: any, extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
8
|
+
contacts(pubkeys?: string[]): import("nostr-tools").Event;
|
|
9
|
+
list(tags?: string[][], extra?: Partial<NostrEvent>): import("nostr-tools").Event;
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { unixNow } from "applesauce-core/helpers";
|
|
2
|
+
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import { finalizeEvent, getPublicKey, kinds } from "nostr-tools";
|
|
5
|
+
export class FakeUser extends SimpleSigner {
|
|
6
|
+
pubkey = getPublicKey(this.key);
|
|
7
|
+
event(data) {
|
|
8
|
+
return finalizeEvent({
|
|
9
|
+
kind: data?.kind ?? kinds.ShortTextNote,
|
|
10
|
+
content: data?.content || "",
|
|
11
|
+
created_at: data?.created_at ?? unixNow(),
|
|
12
|
+
tags: data?.tags || [],
|
|
13
|
+
}, this.key);
|
|
14
|
+
}
|
|
15
|
+
note(content = "Hello World", extra) {
|
|
16
|
+
return this.event({ kind: kinds.ShortTextNote, content, ...extra });
|
|
17
|
+
}
|
|
18
|
+
profile(profile, extra) {
|
|
19
|
+
return this.event({ kind: kinds.Metadata, content: JSON.stringify({ ...profile }), ...extra });
|
|
20
|
+
}
|
|
21
|
+
contacts(pubkeys = []) {
|
|
22
|
+
return this.event({ kind: kinds.Contacts, tags: pubkeys.map((p) => ["p", p]) });
|
|
23
|
+
}
|
|
24
|
+
list(tags = [], extra) {
|
|
25
|
+
return this.event({
|
|
26
|
+
kind: kinds.Bookmarksets,
|
|
27
|
+
content: "",
|
|
28
|
+
tags: [["d", nanoid()], ...tags],
|
|
29
|
+
...extra,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EventStore } from "applesauce-core";
|
|
2
|
+
import { EventFactory } from "applesauce-factory";
|
|
3
|
+
import { NostrEvent } from "nostr-tools";
|
|
4
|
+
/**
|
|
5
|
+
* A callback used to tell the upstream app to publish an event
|
|
6
|
+
* @param label a label describing what
|
|
7
|
+
*/
|
|
8
|
+
export type PublishMethod = (label: string, event: NostrEvent, explicitRelays?: string[]) => Promise<void>;
|
|
9
|
+
/** The context that is passed to actions for them to use to preform actions */
|
|
10
|
+
export type ActionContext = {
|
|
11
|
+
/** The event store to load events from */
|
|
12
|
+
events: EventStore;
|
|
13
|
+
/** The pubkey of the signer in the event factory */
|
|
14
|
+
self: string;
|
|
15
|
+
/** The event factory used to build and modify events */
|
|
16
|
+
factory: EventFactory;
|
|
17
|
+
/** A method used to publish final events to relays */
|
|
18
|
+
publish: PublishMethod;
|
|
19
|
+
};
|
|
20
|
+
/** An action that can be run in a context to preform an action */
|
|
21
|
+
export type Action<T extends unknown = unknown> = (ctx: ActionContext) => Promise<T>;
|
|
22
|
+
export type ActionConstructor<Args extends Array<any>, T extends unknown = unknown> = (...args: Args) => Action<T>;
|
|
23
|
+
/** The main class that runs actions */
|
|
24
|
+
export declare class ActionHub {
|
|
25
|
+
events: EventStore;
|
|
26
|
+
factory: EventFactory;
|
|
27
|
+
publish: PublishMethod;
|
|
28
|
+
constructor(events: EventStore, factory: EventFactory, publish: PublishMethod);
|
|
29
|
+
protected context: ActionContext | undefined;
|
|
30
|
+
getContext(): Promise<ActionContext>;
|
|
31
|
+
run<Args extends Array<any>, T extends unknown = unknown>(Action: ActionConstructor<Args, T>, ...args: Args): Promise<T>;
|
|
32
|
+
follow(pubkey: string): Promise<unknown>;
|
|
33
|
+
unfollow(pubkey: string): Promise<unknown>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FollowUser } from "./actions/contacts.js";
|
|
2
|
+
/** The main class that runs actions */
|
|
3
|
+
export class ActionHub {
|
|
4
|
+
events;
|
|
5
|
+
factory;
|
|
6
|
+
publish;
|
|
7
|
+
constructor(events, factory, publish) {
|
|
8
|
+
this.events = events;
|
|
9
|
+
this.factory = factory;
|
|
10
|
+
this.publish = publish;
|
|
11
|
+
/** The the context observable to get the pubkey */
|
|
12
|
+
// this.context = defer(() => {
|
|
13
|
+
// if (!this.factory.context.signer) throw new Error("Missing signer");
|
|
14
|
+
// return from(this.factory.context.signer.getPublicKey());
|
|
15
|
+
// }).pipe(map((self) => ({ self, events: this.events, factory: this.factory, publish: this.publish })));
|
|
16
|
+
}
|
|
17
|
+
// log = new Subject<{ label: string; args: Array<any>; result: any }>();
|
|
18
|
+
context = undefined;
|
|
19
|
+
async getContext() {
|
|
20
|
+
if (this.context)
|
|
21
|
+
return this.context;
|
|
22
|
+
else {
|
|
23
|
+
if (!this.factory.context.signer)
|
|
24
|
+
throw new Error("Missing signer");
|
|
25
|
+
const self = await this.factory.context.signer.getPublicKey();
|
|
26
|
+
this.context = { self, events: this.events, factory: this.factory, publish: this.publish };
|
|
27
|
+
return this.context;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async run(
|
|
31
|
+
// label: string,
|
|
32
|
+
Action, ...args) {
|
|
33
|
+
const action = Action(...args);
|
|
34
|
+
const ctx = await this.getContext();
|
|
35
|
+
return await action(ctx);
|
|
36
|
+
// const log = { label, args, result };
|
|
37
|
+
// this.log.next(log);
|
|
38
|
+
// return await result;
|
|
39
|
+
}
|
|
40
|
+
// helper methods
|
|
41
|
+
follow(pubkey) {
|
|
42
|
+
return this.run(FollowUser, pubkey);
|
|
43
|
+
}
|
|
44
|
+
unfollow(pubkey) {
|
|
45
|
+
return this.run(FollowUser, pubkey);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vitest } from "vitest";
|
|
2
|
+
import { FakeUser } from "../../__tests__/fake-user.js";
|
|
3
|
+
import { EventFactory } from "applesauce-factory";
|
|
4
|
+
import { EventStore } from "applesauce-core";
|
|
5
|
+
import { ActionHub } from "../../action-hub.js";
|
|
6
|
+
import { FollowUser, NewContacts, UnfollowUser } from "../contacts.js";
|
|
7
|
+
const user = new FakeUser();
|
|
8
|
+
let events;
|
|
9
|
+
let factory;
|
|
10
|
+
let publish;
|
|
11
|
+
let hub;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
events = new EventStore();
|
|
14
|
+
factory = new EventFactory({ signer: user });
|
|
15
|
+
publish = vitest.fn().mockResolvedValue(undefined);
|
|
16
|
+
hub = new ActionHub(events, factory, publish);
|
|
17
|
+
});
|
|
18
|
+
describe("FollowUser", () => {
|
|
19
|
+
it("should throw an error if contacts does not exist", async () => {
|
|
20
|
+
// don't add any events to the store
|
|
21
|
+
await expect(hub.run(FollowUser, user.pubkey)).rejects.toThrow();
|
|
22
|
+
expect(publish).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
it('should publish an event with a new "p" tag', async () => {
|
|
25
|
+
events.add(user.contacts());
|
|
26
|
+
await hub.run(FollowUser, user.pubkey);
|
|
27
|
+
expect(publish).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ tags: expect.arrayContaining([["p", user.pubkey]]) }));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe("UnfollowUser", () => {
|
|
31
|
+
it("should throw an error if contacts does not exist", async () => {
|
|
32
|
+
// don't add any events to the store
|
|
33
|
+
await expect(hub.run(UnfollowUser, user.pubkey)).rejects.toThrow();
|
|
34
|
+
expect(publish).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
it('should publish an event with a new "p" tag', async () => {
|
|
37
|
+
events.add(user.contacts([user.pubkey]));
|
|
38
|
+
await hub.run(UnfollowUser, user.pubkey);
|
|
39
|
+
expect(publish).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ kind: 3, tags: [] }));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("NewContacts", () => {
|
|
43
|
+
it("should throw if contact list already exists", async () => {
|
|
44
|
+
events.add(user.contacts([user.pubkey]));
|
|
45
|
+
await expect(hub.run(NewContacts, [])).rejects.toThrow();
|
|
46
|
+
expect(publish).not.toBeCalled();
|
|
47
|
+
});
|
|
48
|
+
it("should publish a new contact event with pubkeys", async () => {
|
|
49
|
+
await hub.run(NewContacts, [user.pubkey]);
|
|
50
|
+
expect(publish).toHaveBeenCalled();
|
|
51
|
+
expect(publish).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ kind: 3, tags: expect.arrayContaining([["p", user.pubkey]]) }));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Action } from "../action-hub.js";
|
|
2
|
+
/** An action that adds a pubkey to a users contacts event */
|
|
3
|
+
export declare function FollowUser(pubkey: string, relay?: string, hidden?: boolean): Action;
|
|
4
|
+
/** An action that removes a pubkey from a users contacts event */
|
|
5
|
+
export declare function UnfollowUser(pubkey: string, hidden?: boolean): Action;
|
|
6
|
+
/** An action that creates a new kind 3 contacts lists, throws if a contact list already exists */
|
|
7
|
+
export declare function NewContacts(pubkeys?: string[]): Action;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { kinds } from "nostr-tools";
|
|
2
|
+
import { addPubkeyTag, removePubkeyTag } from "applesauce-factory/operations/tag";
|
|
3
|
+
/** An action that adds a pubkey to a users contacts event */
|
|
4
|
+
export function FollowUser(pubkey, relay, hidden = false) {
|
|
5
|
+
return async ({ events, factory, self, publish }) => {
|
|
6
|
+
const contacts = events.getReplaceable(kinds.Contacts, self);
|
|
7
|
+
if (!contacts)
|
|
8
|
+
throw new Error("Missing contacts event");
|
|
9
|
+
const pointer = { pubkey, relays: relay ? [relay] : undefined };
|
|
10
|
+
const operation = addPubkeyTag(pointer);
|
|
11
|
+
const draft = await factory.modifyTags(contacts, hidden ? { hidden: operation } : operation);
|
|
12
|
+
await publish("Update contacts", await factory.sign(draft));
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** An action that removes a pubkey from a users contacts event */
|
|
16
|
+
export function UnfollowUser(pubkey, hidden = false) {
|
|
17
|
+
return async ({ events, factory, self, publish }) => {
|
|
18
|
+
const contacts = events.getReplaceable(kinds.Contacts, self);
|
|
19
|
+
if (!contacts)
|
|
20
|
+
throw new Error("Missing contacts event");
|
|
21
|
+
const operation = removePubkeyTag(pubkey);
|
|
22
|
+
const draft = await factory.modifyTags(contacts, hidden ? { hidden: operation } : operation);
|
|
23
|
+
await publish("Update contacts", await factory.sign(draft));
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** An action that creates a new kind 3 contacts lists, throws if a contact list already exists */
|
|
27
|
+
export function NewContacts(pubkeys) {
|
|
28
|
+
return async ({ events, factory, self, publish }) => {
|
|
29
|
+
const contacts = events.getReplaceable(kinds.Contacts, self);
|
|
30
|
+
if (contacts)
|
|
31
|
+
throw new Error("Contact list already exists");
|
|
32
|
+
const draft = await factory.process({ kind: kinds.Contacts, tags: pubkeys?.map((p) => ["p", p]) });
|
|
33
|
+
await publish("New contact list", await factory.sign(draft));
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./contacts.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./contacts.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from "./action-hub.js";
|
|
2
|
+
export * as Actions from "./actions/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from "./action-hub.js";
|
|
2
|
+
export * as Actions from "./actions/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-actions",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-20250311125119",
|
|
4
4
|
"description": "A package for performing common nostr actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,17 +19,27 @@
|
|
|
19
19
|
"import": "./dist/index.js",
|
|
20
20
|
"require": "./dist/index.js",
|
|
21
21
|
"types": "./dist/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./actions": {
|
|
24
|
+
"import": "./dist/actions/index.js",
|
|
25
|
+
"require": "./dist/actions/index.js",
|
|
26
|
+
"types": "./dist/actions/index.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./actions/*": {
|
|
29
|
+
"import": "./dist/actions/*.js",
|
|
30
|
+
"require": "./dist/actions/*.js",
|
|
31
|
+
"types": "./dist/actions/*.d.ts"
|
|
22
32
|
}
|
|
23
33
|
},
|
|
24
34
|
"dependencies": {
|
|
25
|
-
"applesauce-core": "0.0.0-next-
|
|
26
|
-
"applesauce-factory": "0.0.0-next-
|
|
27
|
-
"
|
|
28
|
-
"nostr-tools": "^2.10.4",
|
|
29
|
-
"rxjs": "^7.8.1"
|
|
35
|
+
"applesauce-core": "0.0.0-next-20250311125119",
|
|
36
|
+
"applesauce-factory": "0.0.0-next-20250311125119",
|
|
37
|
+
"nostr-tools": "^2.10.4"
|
|
30
38
|
},
|
|
31
39
|
"devDependencies": {
|
|
32
40
|
"@types/debug": "^4.1.12",
|
|
41
|
+
"applesauce-signers": "0.0.0-next-20250311125119",
|
|
42
|
+
"nanoid": "^5.0.9",
|
|
33
43
|
"typescript": "^5.7.3",
|
|
34
44
|
"vitest": "^3.0.5"
|
|
35
45
|
},
|