applesauce-actions 0.11.0 → 0.12.0

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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Applesauce Actions
2
+
3
+ A collection of pre-built actions nostr clients can use. built on top of `applesauce-core` and `applesauce-factory`
4
+
5
+ [Documentation](https://hzrd149.github.io/applesauce/typedoc/modules/applesauce_actions.html)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { from, Subject } from "rxjs";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
4
+ import { EventFactory } from "applesauce-factory";
5
+ import { EventStore } from "applesauce-core";
6
+ import { FakeUser } from "./fake-user.js";
7
+ import { ActionHub } from "../action-hub.js";
8
+ import { CreateProfile } from "../actions/profile.js";
9
+ const user = new FakeUser();
10
+ const events = new EventStore();
11
+ const factory = new EventFactory({ signer: user });
12
+ describe("runAction", () => {
13
+ it("should handle action that return observables", async () => {
14
+ const e = [user.note(), user.profile({ name: "testing" })];
15
+ const action = () => from(e);
16
+ const spy = subscribeSpyTo(ActionHub.runAction({ events, factory, self: await user.getPublicKey() }, action));
17
+ await spy.onComplete();
18
+ expect(spy.getValues()).toEqual(e);
19
+ });
20
+ it("should handle action that return AsyncIterable", async () => {
21
+ const e = [user.note(), user.profile({ name: "testing" })];
22
+ async function* action() {
23
+ for (const event of e)
24
+ yield event;
25
+ }
26
+ const spy = subscribeSpyTo(ActionHub.runAction({ events, factory, self: await user.getPublicKey() }, action));
27
+ await spy.onComplete();
28
+ expect(spy.getValues()).toEqual(e);
29
+ });
30
+ it("should handle action that return Iterable", async () => {
31
+ const e = [user.note(), user.profile({ name: "testing" })];
32
+ function* action() {
33
+ for (const event of e)
34
+ yield event;
35
+ }
36
+ const spy = subscribeSpyTo(ActionHub.runAction({ events, factory, self: await user.getPublicKey() }, action));
37
+ await spy.onComplete();
38
+ expect(spy.getValues()).toEqual(e);
39
+ });
40
+ });
41
+ describe("run", () => {
42
+ it("should throw if publish is not set", async () => {
43
+ const hub = new ActionHub(events, factory);
44
+ await expect(async () => hub.run(CreateProfile, { name: "fiatjaf" })).rejects.toThrow();
45
+ });
46
+ it("should call publish with all events", async () => {
47
+ const publish = vi.fn().mockResolvedValue(undefined);
48
+ const hub = new ActionHub(events, factory, publish);
49
+ await hub.run(CreateProfile, { name: "fiatjaf" });
50
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({ content: JSON.stringify({ name: "fiatjaf" }) }));
51
+ });
52
+ });
53
+ describe("exec", () => {
54
+ it("should support forEach to stream to publish", async () => {
55
+ const publish = vi.fn().mockResolvedValue(undefined);
56
+ const hub = new ActionHub(events, factory);
57
+ await hub.exec(CreateProfile, { name: "fiatjaf" }).forEach(publish);
58
+ expect(publish).toHaveBeenCalledWith(expect.objectContaining({ content: JSON.stringify({ name: "fiatjaf" }) }));
59
+ });
60
+ it("should support streaming to a publish subject", async () => {
61
+ const publish = new Subject();
62
+ const spy = subscribeSpyTo(publish);
63
+ const hub = new ActionHub(events, factory);
64
+ await hub.exec(CreateProfile, { name: "fiatjaf" }).forEach((v) => publish.next(v));
65
+ expect(spy.getValues()).toEqual([expect.objectContaining({ content: JSON.stringify({ name: "fiatjaf" }) })]);
66
+ });
67
+ });
@@ -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,36 @@
1
+ import { Observable } from "rxjs";
2
+ import { NostrEvent } from "nostr-tools";
3
+ import { IEventStore } from "applesauce-core/event-store";
4
+ import { EventFactory } from "applesauce-factory";
5
+ /**
6
+ * A callback used to tell the upstream app to publish an event
7
+ * @param label a label describing what
8
+ */
9
+ export type PublishMethod = (event: NostrEvent) => void | Promise<void>;
10
+ /** The context that is passed to actions for them to use to preform actions */
11
+ export type ActionContext = {
12
+ /** The event store to load events from */
13
+ events: IEventStore;
14
+ /** The pubkey of the signer in the event factory */
15
+ self: string;
16
+ /** The event factory used to build and modify events */
17
+ factory: EventFactory;
18
+ };
19
+ /** An action that can be run in a context to preform an action */
20
+ export type Action = (ctx: ActionContext) => Observable<NostrEvent> | AsyncGenerator<NostrEvent> | Generator<NostrEvent>;
21
+ export type ActionConstructor<Args extends Array<any>> = (...args: Args) => Action;
22
+ /** The main class that runs actions */
23
+ export declare class ActionHub {
24
+ events: IEventStore;
25
+ factory: EventFactory;
26
+ publish?: PublishMethod | undefined;
27
+ constructor(events: IEventStore, factory: EventFactory, publish?: PublishMethod | undefined);
28
+ protected context: ActionContext | undefined;
29
+ protected getContext(): Promise<ActionContext>;
30
+ /** Runs an action in a ActionContext and converts the result to an Observable */
31
+ static runAction(ctx: ActionContext, action: Action): Observable<NostrEvent>;
32
+ /** Run an action and publish events using the publish method */
33
+ run<Args extends Array<any>>(Action: ActionConstructor<Args>, ...args: Args): Promise<void>;
34
+ /** Run an action without publishing the events */
35
+ exec<Args extends Array<any>>(Action: ActionConstructor<Args>, ...args: Args): Observable<NostrEvent>;
36
+ }
@@ -0,0 +1,49 @@
1
+ import { from, isObservable, lastValueFrom, switchMap, toArray } from "rxjs";
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
+ }
12
+ context = undefined;
13
+ async getContext() {
14
+ if (this.context)
15
+ return this.context;
16
+ else {
17
+ if (!this.factory.context.signer)
18
+ throw new Error("Missing signer");
19
+ const self = await this.factory.context.signer.getPublicKey();
20
+ this.context = { self, events: this.events, factory: this.factory };
21
+ return this.context;
22
+ }
23
+ }
24
+ /** Runs an action in a ActionContext and converts the result to an Observable */
25
+ static runAction(ctx, action) {
26
+ const result = action(ctx);
27
+ if (isObservable(result))
28
+ return result;
29
+ else
30
+ return from(result);
31
+ }
32
+ /** Run an action and publish events using the publish method */
33
+ async run(Action, ...args) {
34
+ if (!this.publish)
35
+ throw new Error("Missing publish method, use ActionHub.exec");
36
+ // wait for action to complete and group events
37
+ const events = await lastValueFrom(this.exec(Action, ...args).pipe(toArray()));
38
+ // publish events
39
+ for (const event of events)
40
+ await this.publish(event);
41
+ }
42
+ /** Run an action without publishing the events */
43
+ exec(Action, ...args) {
44
+ return from(this.getContext()).pipe(switchMap((ctx) => {
45
+ const action = Action(...args);
46
+ return ActionHub.runAction(ctx, action);
47
+ }));
48
+ }
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { beforeEach, describe, expect, it, vitest } from "vitest";
2
+ import { EventStore } from "applesauce-core";
3
+ import { EventFactory } from "applesauce-factory";
4
+ import { kinds } from "nostr-tools";
5
+ import { FakeUser } from "../../__tests__/fake-user.js";
6
+ import { ActionHub } from "../../action-hub.js";
7
+ import { CreateBookmarkList } from "../bookmarks.js";
8
+ const user = new FakeUser();
9
+ let events;
10
+ let factory;
11
+ let publish;
12
+ let hub;
13
+ beforeEach(() => {
14
+ events = new EventStore();
15
+ factory = new EventFactory({ signer: user });
16
+ publish = vitest.fn().mockResolvedValue(undefined);
17
+ hub = new ActionHub(events, factory, publish);
18
+ });
19
+ describe("CreateBookmarkList", () => {
20
+ it("should publish a kind 10003 bookmark list", async () => {
21
+ await hub.run(CreateBookmarkList);
22
+ expect(publish).toBeCalledWith(expect.objectContaining({ kind: kinds.BookmarkList }));
23
+ });
24
+ });
@@ -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.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.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.objectContaining({ kind: 3, tags: expect.arrayContaining([["p", user.pubkey]]) }));
52
+ });
53
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { EventStore } from "applesauce-core";
3
+ import { EventFactory } from "applesauce-factory";
4
+ import { kinds } from "nostr-tools";
5
+ import { unlockHiddenTags } from "applesauce-core/helpers";
6
+ import { subscribeSpyTo } from "@hirez_io/observer-spy";
7
+ import { FakeUser } from "../../__tests__/fake-user.js";
8
+ import { ActionHub } from "../../action-hub.js";
9
+ import { AddUserToFollowSet, RemoveUserFromFollowSet } from "../follow-sets.js";
10
+ const user = new FakeUser();
11
+ const testPubkey = "test-pubkey";
12
+ const testIdentifier = "test-list";
13
+ let events;
14
+ let factory;
15
+ let hub;
16
+ beforeEach(() => {
17
+ events = new EventStore();
18
+ factory = new EventFactory({ signer: user });
19
+ hub = new ActionHub(events, factory);
20
+ // Add a follow set event to work with
21
+ const followSet = user.event({
22
+ kind: kinds.Followsets,
23
+ tags: [["d", testIdentifier]],
24
+ content: "",
25
+ created_at: Math.floor(Date.now() / 1000),
26
+ });
27
+ events.add(followSet);
28
+ });
29
+ describe("AddUserToList", () => {
30
+ it("should add a pubkey to public tags in a follow set", async () => {
31
+ const spy = subscribeSpyTo(hub.exec(AddUserToFollowSet, testPubkey, testIdentifier), { expectErrors: false });
32
+ await spy.onComplete();
33
+ const emittedEvent = spy.getLastValue();
34
+ expect(emittedEvent).toMatchObject({
35
+ kind: kinds.Followsets,
36
+ tags: expect.arrayContaining([
37
+ ["d", testIdentifier],
38
+ ["p", testPubkey],
39
+ ]),
40
+ });
41
+ });
42
+ it("should add a pubkey to hidden tags in a follow set", async () => {
43
+ const spy = subscribeSpyTo(hub.exec(AddUserToFollowSet, testPubkey, testIdentifier, true), { expectErrors: false });
44
+ await spy.onComplete();
45
+ const emittedEvent = spy.getLastValue();
46
+ expect(await unlockHiddenTags(emittedEvent, user)).toEqual(expect.arrayContaining([["p", testPubkey]]));
47
+ });
48
+ });
49
+ describe("RemoveUserFromList", () => {
50
+ beforeEach(async () => {
51
+ // Add a follow set with existing tags to remove
52
+ const followSetWithTags = user.event({
53
+ kind: kinds.Followsets,
54
+ tags: [
55
+ ["d", testIdentifier],
56
+ ["p", testPubkey],
57
+ ],
58
+ content: await user.nip04.encrypt(user.pubkey, JSON.stringify(["p", testPubkey])),
59
+ created_at: Math.floor(Date.now() / 1000),
60
+ });
61
+ events.add(followSetWithTags);
62
+ });
63
+ it("should remove a pubkey from public tags in a follow set", async () => {
64
+ const spy = subscribeSpyTo(hub.exec(RemoveUserFromFollowSet, testPubkey, testIdentifier), { expectErrors: false });
65
+ await spy.onComplete();
66
+ const emittedEvent = spy.getLastValue();
67
+ expect(emittedEvent).toMatchObject({
68
+ kind: kinds.Followsets,
69
+ tags: expect.not.arrayContaining([["p", testPubkey]]),
70
+ });
71
+ });
72
+ it("should remove a pubkey from hidden tags in a follow set", async () => {
73
+ const spy = subscribeSpyTo(hub.exec(RemoveUserFromFollowSet, testPubkey, testIdentifier, true), {
74
+ expectErrors: false,
75
+ });
76
+ await spy.onComplete();
77
+ const emittedEvent = spy.getLastValue();
78
+ expect(await unlockHiddenTags(emittedEvent, user)).toEqual(expect.not.arrayContaining([["hidden", "p", testPubkey]]));
79
+ });
80
+ });
@@ -0,0 +1,24 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Action } from "../action-hub.js";
3
+ /**
4
+ * An action that adds a note or article to the bookmark list or a bookmark set
5
+ * @param event the event to bookmark
6
+ * @param identifier the "d" tag of the bookmark set
7
+ * @param hidden set to true to add to hidden bookmarks
8
+ */
9
+ export declare function BookmarkEvent(event: NostrEvent, identifier?: string, hidden?: boolean): Action;
10
+ /**
11
+ * An action that removes a note or article from the bookmark list or bookmark set
12
+ * @param event the event to remove from bookmarks
13
+ * @param identifier the "d" tag of the bookmark set
14
+ * @param hidden set to true to remove from hidden bookmarks
15
+ */
16
+ export declare function UnbookmarkEvent(event: NostrEvent, identifier: string, hidden?: boolean): Action;
17
+ /** An action that creates a new bookmark list for a user */
18
+ export declare function CreateBookmarkList(bookmarks?: NostrEvent[]): Action;
19
+ /** An action that creates a new bookmark set for a user */
20
+ export declare function CreateBookmarkSet(title: string, description: string, additional: {
21
+ image?: string;
22
+ hidden?: NostrEvent[];
23
+ public?: NostrEvent[];
24
+ }): Action;
@@ -0,0 +1,58 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { addEventBookmarkTag, removeEventBookmarkTag } from "applesauce-factory/operations/tag";
3
+ import { modifyHiddenTags, modifyPublicTags, setListDescription, setListImage, setListTitle, } from "applesauce-factory/operations/event";
4
+ function getBookmarkEvent(events, self, identifier) {
5
+ return events.getReplaceable(identifier ? kinds.Bookmarksets : kinds.BookmarkList, self, identifier);
6
+ }
7
+ /**
8
+ * An action that adds a note or article to the bookmark list or a bookmark set
9
+ * @param event the event to bookmark
10
+ * @param identifier the "d" tag of the bookmark set
11
+ * @param hidden set to true to add to hidden bookmarks
12
+ */
13
+ export function BookmarkEvent(event, identifier, hidden = false) {
14
+ return async function* ({ events, factory, self }) {
15
+ const bookmarks = getBookmarkEvent(events, self, identifier);
16
+ if (!bookmarks)
17
+ throw new Error("Cant find bookmarks");
18
+ const operation = addEventBookmarkTag(event);
19
+ const draft = await factory.modifyTags(bookmarks, hidden ? { hidden: operation } : operation);
20
+ yield await factory.sign(draft);
21
+ };
22
+ }
23
+ /**
24
+ * An action that removes a note or article from the bookmark list or bookmark set
25
+ * @param event the event to remove from bookmarks
26
+ * @param identifier the "d" tag of the bookmark set
27
+ * @param hidden set to true to remove from hidden bookmarks
28
+ */
29
+ export function UnbookmarkEvent(event, identifier, hidden = false) {
30
+ return async function* ({ events, factory, self }) {
31
+ const bookmarks = getBookmarkEvent(events, self, identifier);
32
+ if (!bookmarks)
33
+ throw new Error("Cant find bookmarks");
34
+ const operation = removeEventBookmarkTag(event);
35
+ const draft = await factory.modifyTags(bookmarks, hidden ? { hidden: operation } : operation);
36
+ yield await factory.sign(draft);
37
+ };
38
+ }
39
+ /** An action that creates a new bookmark list for a user */
40
+ export function CreateBookmarkList(bookmarks) {
41
+ return async function* ({ events, factory, self }) {
42
+ const existing = getBookmarkEvent(events, self);
43
+ if (existing)
44
+ throw new Error("Bookmark list already exists");
45
+ const draft = await factory.build({ kind: kinds.BookmarkList }, bookmarks ? modifyPublicTags(...bookmarks.map(addEventBookmarkTag)) : undefined);
46
+ yield await factory.sign(draft);
47
+ };
48
+ }
49
+ /** An action that creates a new bookmark set for a user */
50
+ export function CreateBookmarkSet(title, description, additional) {
51
+ return async function* ({ events, factory, self }) {
52
+ const existing = getBookmarkEvent(events, self);
53
+ if (existing)
54
+ throw new Error("Bookmark list already exists");
55
+ const draft = await factory.process({ kind: kinds.BookmarkList }, setListTitle(title), setListDescription(description), additional.image ? setListImage(additional.image) : undefined, additional.public ? modifyPublicTags(...additional.public.map(addEventBookmarkTag)) : undefined, additional.hidden ? modifyHiddenTags(...additional.hidden.map(addEventBookmarkTag)) : undefined);
56
+ yield await factory.sign(draft);
57
+ };
58
+ }
@@ -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 function* ({ events, factory, self }) {
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
+ yield 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 function* ({ events, factory, self }) {
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
+ yield 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 function* ({ events, factory, self }) {
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
+ yield await factory.sign(draft);
34
+ };
35
+ }
@@ -0,0 +1,17 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Action } from "../action-hub.js";
3
+ import { ProfilePointer } from "nostr-tools/nip19";
4
+ /**
5
+ * An action that adds a pubkey to a follow set
6
+ * @param pubkey the pubkey to add to the set
7
+ * @param identifier the "d" tag of the follow set
8
+ * @param hidden set to true to add to hidden follows
9
+ */
10
+ export declare function AddUserToFollowSet(pubkey: string | ProfilePointer, identifier: NostrEvent | string, hidden?: boolean): Action;
11
+ /**
12
+ * An action that removes a pubkey from a follow set
13
+ * @param pubkey the pubkey to remove from the set
14
+ * @param identifier the "d" tag of the follow set
15
+ * @param hidden set to true to remove from hidden follows
16
+ */
17
+ export declare function RemoveUserFromFollowSet(pubkey: string | ProfilePointer, identifier: NostrEvent | string, hidden?: boolean): Action;
@@ -0,0 +1,38 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { addPubkeyTag, removePubkeyTag } from "applesauce-factory/operations/tag";
3
+ function getFollowSetEvent(events, self, identifier) {
4
+ const set = typeof identifier === "string" ? events.getReplaceable(kinds.Followsets, self, identifier) : identifier;
5
+ if (!set)
6
+ throw new Error("Can't find follow set");
7
+ if (set.kind !== kinds.Followsets)
8
+ throw new Error("Event is not a follow set");
9
+ return set;
10
+ }
11
+ /**
12
+ * An action that adds a pubkey to a follow set
13
+ * @param pubkey the pubkey to add to the set
14
+ * @param identifier the "d" tag of the follow set
15
+ * @param hidden set to true to add to hidden follows
16
+ */
17
+ export function AddUserToFollowSet(pubkey, identifier, hidden = false) {
18
+ return async function* ({ events, factory, self }) {
19
+ const follows = getFollowSetEvent(events, self, identifier);
20
+ const operation = addPubkeyTag(pubkey);
21
+ const draft = await factory.modifyTags(follows, hidden ? { hidden: operation } : operation);
22
+ yield await factory.sign(draft);
23
+ };
24
+ }
25
+ /**
26
+ * An action that removes a pubkey from a follow set
27
+ * @param pubkey the pubkey to remove from the set
28
+ * @param identifier the "d" tag of the follow set
29
+ * @param hidden set to true to remove from hidden follows
30
+ */
31
+ export function RemoveUserFromFollowSet(pubkey, identifier, hidden = false) {
32
+ return async function* ({ events, factory, self }) {
33
+ const follows = getFollowSetEvent(events, self, identifier);
34
+ const operation = removePubkeyTag(pubkey);
35
+ const draft = await factory.modifyTags(follows, hidden ? { hidden: operation } : operation);
36
+ yield await factory.sign(draft);
37
+ };
38
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./bookmarks.js";
2
+ export * from "./contacts.js";
3
+ export * from "./follow-sets.js";
4
+ export * from "./pins.js";
5
+ export * from "./profile.js";
@@ -0,0 +1,5 @@
1
+ export * from "./bookmarks.js";
2
+ export * from "./contacts.js";
3
+ export * from "./follow-sets.js";
4
+ export * from "./pins.js";
5
+ export * from "./profile.js";
@@ -0,0 +1,8 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Action } from "../action-hub.js";
3
+ /** An action that pins a note to the users pin list */
4
+ export declare function PinNote(note: NostrEvent): Action;
5
+ /** An action that removes an event from the users pin list */
6
+ export declare function UnpinNote(note: NostrEvent): Action;
7
+ /** An action that creates a new pin list for a user */
8
+ export declare function CreatePinList(pins?: NostrEvent[]): Action;
@@ -0,0 +1,33 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { addEventTag, removeEventTag } from "applesauce-factory/operations/tag";
3
+ import { modifyPublicTags } from "applesauce-factory/operations/event";
4
+ /** An action that pins a note to the users pin list */
5
+ export function PinNote(note) {
6
+ return async function* ({ events, factory, self }) {
7
+ const pins = events.getReplaceable(kinds.Pinlist, self);
8
+ if (!pins)
9
+ throw new Error("Missing pin list");
10
+ const draft = await factory.modifyTags(pins, addEventTag(note.id));
11
+ yield await factory.sign(draft);
12
+ };
13
+ }
14
+ /** An action that removes an event from the users pin list */
15
+ export function UnpinNote(note) {
16
+ return async function* ({ events, factory, self }) {
17
+ const pins = events.getReplaceable(kinds.Pinlist, self);
18
+ if (!pins)
19
+ throw new Error("Missing pin list");
20
+ const draft = await factory.modifyTags(pins, removeEventTag(note.id));
21
+ yield await factory.sign(draft);
22
+ };
23
+ }
24
+ /** An action that creates a new pin list for a user */
25
+ export function CreatePinList(pins = []) {
26
+ return async function* ({ events, factory, self }) {
27
+ const existing = events.getReplaceable(kinds.Pinlist, self);
28
+ if (existing)
29
+ throw new Error("Pin list already exists");
30
+ const draft = await factory.process({ kind: kinds.Pinlist }, modifyPublicTags(...pins.map((event) => addEventTag(event.id))));
31
+ yield await factory.sign(draft);
32
+ };
33
+ }
@@ -0,0 +1,6 @@
1
+ import { ProfileContent } from "applesauce-core/helpers";
2
+ import { Action } from "../action-hub.js";
3
+ /** An action that creates a new kind 0 profile event for a user */
4
+ export declare function CreateProfile(content: ProfileContent): Action;
5
+ /** An action that updates a kind 0 profile evnet for a user */
6
+ export declare function UpdateProfile(content: Partial<ProfileContent>): Action;
@@ -0,0 +1,22 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { setProfileContent, updateProfileContent } from "applesauce-factory/operations/event";
3
+ /** An action that creates a new kind 0 profile event for a user */
4
+ export function CreateProfile(content) {
5
+ return async function* ({ events, factory, self }) {
6
+ const metadata = events.getReplaceable(kinds.Metadata, self);
7
+ if (metadata)
8
+ throw new Error("Profile already exists");
9
+ const draft = await factory.build({ kind: kinds.Metadata }, setProfileContent(content));
10
+ yield await factory.sign(draft);
11
+ };
12
+ }
13
+ /** An action that updates a kind 0 profile evnet for a user */
14
+ export function UpdateProfile(content) {
15
+ return async function* ({ events, factory, self }) {
16
+ const metadata = events.getReplaceable(kinds.Metadata, self);
17
+ if (!metadata)
18
+ throw new Error("Profile does not exists");
19
+ const draft = await factory.modify(metadata, updateProfileContent(content));
20
+ yield await factory.sign(draft);
21
+ };
22
+ }
@@ -0,0 +1,3 @@
1
+ import { Observable } from "rxjs";
2
+ /** Subscribes to an observable, send all the values to a method and unsubscribes when complete */
3
+ export declare function play<T extends unknown = unknown>(stream: Observable<T>, method: (value: T) => void): Promise<void>;
@@ -0,0 +1,20 @@
1
+ /** Subscribes to an observable, send all the values to a method and unsubscribes when complete */
2
+ export function play(stream, method) {
3
+ return new Promise((resolve, reject) => {
4
+ const sub = stream.subscribe({
5
+ next: (v) => {
6
+ try {
7
+ method(v);
8
+ }
9
+ catch (error) {
10
+ reject(error);
11
+ }
12
+ },
13
+ error: (error) => reject(error),
14
+ complete: () => {
15
+ sub.unsubscribe();
16
+ resolve();
17
+ },
18
+ });
19
+ });
20
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export declare const actions = 0;
1
+ export * from "./action-hub.js";
2
+ export * as Actions from "./actions/index.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
- export const actions = 0;
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.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "A package for performing common nostr actions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,17 +19,29 @@
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.11.0",
26
- "applesauce-factory": "^0.11.0",
27
- "debug": "^4.4.0",
35
+ "applesauce-core": "^0.12.0",
36
+ "applesauce-factory": "^0.12.0",
28
37
  "nostr-tools": "^2.10.4",
29
38
  "rxjs": "^7.8.1"
30
39
  },
31
40
  "devDependencies": {
41
+ "@hirez_io/observer-spy": "^2.2.0",
32
42
  "@types/debug": "^4.1.12",
43
+ "applesauce-signers": "^0.12.0",
44
+ "nanoid": "^5.0.9",
33
45
  "typescript": "^5.7.3",
34
46
  "vitest": "^3.0.5"
35
47
  },