applesauce-actions 0.11.0 → 1.0.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.
Files changed (50) hide show
  1. package/README.md +42 -0
  2. package/dist/__tests__/action-hub.test.d.ts +1 -0
  3. package/dist/__tests__/action-hub.test.js +67 -0
  4. package/dist/__tests__/fake-user.d.ts +10 -0
  5. package/dist/__tests__/fake-user.js +32 -0
  6. package/dist/action-hub.d.ts +36 -0
  7. package/dist/action-hub.js +49 -0
  8. package/dist/actions/__tests__/blossom.test.d.ts +1 -0
  9. package/dist/actions/__tests__/blossom.test.js +93 -0
  10. package/dist/actions/__tests__/bookmarks.test.d.ts +1 -0
  11. package/dist/actions/__tests__/bookmarks.test.js +24 -0
  12. package/dist/actions/__tests__/contacts.test.d.ts +1 -0
  13. package/dist/actions/__tests__/contacts.test.js +53 -0
  14. package/dist/actions/__tests__/follow-sets.test.d.ts +1 -0
  15. package/dist/actions/__tests__/follow-sets.test.js +80 -0
  16. package/dist/actions/__tests__/mute.test.d.ts +1 -0
  17. package/dist/actions/__tests__/mute.test.js +67 -0
  18. package/dist/actions/blocked-relays.d.ts +10 -0
  19. package/dist/actions/blocked-relays.js +48 -0
  20. package/dist/actions/blossom.d.ts +9 -0
  21. package/dist/actions/blossom.js +52 -0
  22. package/dist/actions/bookmarks.d.ts +24 -0
  23. package/dist/actions/bookmarks.js +58 -0
  24. package/dist/actions/contacts.d.ts +7 -0
  25. package/dist/actions/contacts.js +35 -0
  26. package/dist/actions/dm-relays.d.ts +7 -0
  27. package/dist/actions/dm-relays.js +38 -0
  28. package/dist/actions/favorite-relays.d.ts +18 -0
  29. package/dist/actions/favorite-relays.js +75 -0
  30. package/dist/actions/follow-sets.d.ts +43 -0
  31. package/dist/actions/follow-sets.js +71 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.js +13 -0
  34. package/dist/actions/mailboxes.d.ts +11 -0
  35. package/dist/actions/mailboxes.js +66 -0
  36. package/dist/actions/mute.d.ts +19 -0
  37. package/dist/actions/mute.js +79 -0
  38. package/dist/actions/pins.d.ts +8 -0
  39. package/dist/actions/pins.js +33 -0
  40. package/dist/actions/profile.d.ts +6 -0
  41. package/dist/actions/profile.js +22 -0
  42. package/dist/actions/relay-sets.d.ts +19 -0
  43. package/dist/actions/relay-sets.js +44 -0
  44. package/dist/actions/search-relays.d.ts +10 -0
  45. package/dist/actions/search-relays.js +48 -0
  46. package/dist/helpers/observable.d.ts +3 -0
  47. package/dist/helpers/observable.js +20 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +2 -1
  50. package/package.json +18 -6
package/README.md ADDED
@@ -0,0 +1,42 @@
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)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install applesauce-actions
11
+ ```
12
+
13
+ ## Overview
14
+
15
+ Actions are common pre-built async operations that apps can perform. They use:
16
+
17
+ - `EventStore` for access to known nostr events
18
+ - `EventFactory` to build and sign new nostr events
19
+ - A `publish` method to publish or save the resulting events
20
+
21
+ The package provides an `ActionHub` class that combines these components into a single manager for easier action execution.
22
+
23
+ ## Basic Usage
24
+
25
+ ```typescript
26
+ import { ActionHub } from "applesauce-actions";
27
+ import { FollowUser } from "applesauce-actions/actions";
28
+
29
+ async function publishEvent(event: NostrEvent) {
30
+ await relayPool.publish(event, ["wss://relay.example.com"]);
31
+ }
32
+
33
+ // Create an action hub with your event store, factory and publish method
34
+ const hub = new ActionHub(eventStore, eventFactory, publishEvent);
35
+
36
+ // Example: Follow a user
37
+ await hub
38
+ .exec(FollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
39
+ .forEach((event) => publishEvent(event));
40
+ ```
41
+
42
+ For more detailed documentation and examples, visit the [full documentation](https://hzrd149.github.io/applesauce/overview/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 { ISyncEventStore } 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: ISyncEventStore;
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: ISyncEventStore;
25
+ factory: EventFactory;
26
+ publish?: PublishMethod | undefined;
27
+ constructor(events: ISyncEventStore, 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,93 @@
1
+ import { EventStore } from "applesauce-core";
2
+ import { BLOSSOM_SERVER_LIST_KIND } from "applesauce-core/helpers/blossom";
3
+ import { EventFactory } from "applesauce-factory";
4
+ import { firstValueFrom, lastValueFrom } from "rxjs";
5
+ import { toArray } from "rxjs/operators";
6
+ import { beforeEach, describe, expect, it } from "vitest";
7
+ import { FakeUser } from "../../__tests__/fake-user.js";
8
+ import { ActionHub } from "../../action-hub.js";
9
+ import { AddBlossomServer, NewBlossomServers, RemoveBlossomServer, SetDefaultBlossomServer } from "../blossom.js";
10
+ const user = new FakeUser();
11
+ let events;
12
+ let factory;
13
+ let hub;
14
+ beforeEach(() => {
15
+ events = new EventStore();
16
+ factory = new EventFactory({ signer: user });
17
+ hub = new ActionHub(events, factory);
18
+ });
19
+ describe("NewBlossomServers", () => {
20
+ it("should publish a kind 10063 blossom server list", async () => {
21
+ const result = await lastValueFrom(hub.exec(NewBlossomServers, ["https://cdn.example.com/"]).pipe(toArray()));
22
+ expect(result[0]).toMatchObject({ kind: BLOSSOM_SERVER_LIST_KIND, tags: [["server", "https://cdn.example.com/"]] });
23
+ });
24
+ it("should throw if a blossom servers event already exists", async () => {
25
+ // Create the initial event
26
+ await hub.exec(NewBlossomServers, ["https://cdn.example.com/"]).forEach((e) => events.add(e));
27
+ // Attempt to create another one
28
+ await expect(lastValueFrom(hub.exec(NewBlossomServers, ["https://other.example.com/"]).pipe(toArray()))).rejects.toThrow("Blossom servers event already exists");
29
+ });
30
+ });
31
+ describe("AddBlossomServer", () => {
32
+ beforeEach(async () => {
33
+ // Create an initial empty server list
34
+ await hub.exec(NewBlossomServers, ["https://cdn.example.com/"]).forEach((e) => events.add(e));
35
+ });
36
+ it("should add a single server to the list", async () => {
37
+ const result = await firstValueFrom(hub.exec(AddBlossomServer, "https://other.example.com/"));
38
+ expect(result).toMatchObject({
39
+ kind: BLOSSOM_SERVER_LIST_KIND,
40
+ tags: expect.arrayContaining([["server", expect.stringContaining("other.example.com")]]),
41
+ });
42
+ });
43
+ it("should add multiple servers to the list", async () => {
44
+ const result = await firstValueFrom(hub.exec(AddBlossomServer, ["https://other.example.com/", "https://other2.example.com/"]));
45
+ expect(result).toMatchObject({
46
+ kind: BLOSSOM_SERVER_LIST_KIND,
47
+ tags: expect.arrayContaining([
48
+ ["server", expect.stringContaining("other.example.com")],
49
+ ["server", expect.stringContaining("other2.example.com")],
50
+ ]),
51
+ });
52
+ });
53
+ });
54
+ describe("RemoveBlossomServer", () => {
55
+ beforeEach(async () => {
56
+ // Create an initial server list with two servers
57
+ await hub
58
+ .exec(NewBlossomServers, ["https://cdn.example.com/", "https://other.example.com/"])
59
+ .forEach((e) => events.add(e));
60
+ });
61
+ it("should remove a server from the list", async () => {
62
+ const result = await firstValueFrom(hub.exec(RemoveBlossomServer, "https://cdn.example.com/"));
63
+ expect(result).toMatchObject({
64
+ kind: BLOSSOM_SERVER_LIST_KIND,
65
+ tags: expect.arrayContaining([["server", expect.stringContaining("other.example.com")]]),
66
+ });
67
+ });
68
+ it("should remove multiple servers from the list", async () => {
69
+ const result = await firstValueFrom(hub.exec(RemoveBlossomServer, ["https://cdn.example.com/", "https://other.example.com/"]));
70
+ expect(result).toMatchObject({
71
+ kind: BLOSSOM_SERVER_LIST_KIND,
72
+ tags: [],
73
+ });
74
+ });
75
+ });
76
+ describe("SetDefaultBlossomServer", () => {
77
+ beforeEach(async () => {
78
+ // Create an initial server list with two servers
79
+ await hub
80
+ .exec(NewBlossomServers, ["https://cdn.example.com/", "https://other.example.com/"])
81
+ .forEach((e) => events.add(e));
82
+ });
83
+ it("should move the specified server to the top of the list", async () => {
84
+ const result = await firstValueFrom(hub.exec(SetDefaultBlossomServer, "https://other.example.com/"));
85
+ expect(result).toMatchObject({
86
+ kind: BLOSSOM_SERVER_LIST_KIND,
87
+ tags: expect.arrayContaining([
88
+ ["server", expect.stringContaining("other.example.com")],
89
+ ["server", expect.stringContaining("cdn.example.com")],
90
+ ]),
91
+ });
92
+ });
93
+ });
@@ -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 @@
1
+ export {};
@@ -0,0 +1,67 @@
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 { getMutedThings, getHiddenMutedThings } 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 { MuteThread, UnmuteThread } from "../mute.js";
10
+ const user = new FakeUser();
11
+ const testEventId = "test-event-id";
12
+ let events;
13
+ let factory;
14
+ let hub;
15
+ beforeEach(() => {
16
+ events = new EventStore();
17
+ factory = new EventFactory({ signer: user });
18
+ hub = new ActionHub(events, factory);
19
+ // Add a mute list event to work with
20
+ const muteList = user.event({
21
+ kind: kinds.Mutelist,
22
+ tags: [],
23
+ content: "",
24
+ created_at: Math.floor(Date.now() / 1000),
25
+ });
26
+ events.add(muteList);
27
+ });
28
+ describe("MuteThread", () => {
29
+ it("should add an event to public tags in mute list", async () => {
30
+ const spy = subscribeSpyTo(hub.exec(MuteThread, testEventId), { expectErrors: false });
31
+ await spy.onComplete();
32
+ const emittedEvent = spy.getLastValue();
33
+ const mutedThings = getMutedThings(emittedEvent);
34
+ expect(mutedThings.threads).toContain(testEventId);
35
+ });
36
+ it("should add an event to hidden tags in mute list", async () => {
37
+ const spy = subscribeSpyTo(hub.exec(MuteThread, testEventId, true), { expectErrors: false });
38
+ await spy.onComplete();
39
+ const emittedEvent = spy.getLastValue();
40
+ const hiddenMutedThings = await getHiddenMutedThings(emittedEvent);
41
+ expect(hiddenMutedThings?.threads).toContain(testEventId);
42
+ });
43
+ });
44
+ describe("UnmuteThread", () => {
45
+ it("should remove an event from public tags in mute list", async () => {
46
+ // First add the thread to mute list
47
+ const addSpy = subscribeSpyTo(hub.exec(MuteThread, testEventId), { expectErrors: false });
48
+ await addSpy.onComplete();
49
+ // Then unmute it
50
+ const spy = subscribeSpyTo(hub.exec(UnmuteThread, testEventId), { expectErrors: false });
51
+ await spy.onComplete();
52
+ const emittedEvent = spy.getLastValue();
53
+ const mutedThings = getMutedThings(emittedEvent);
54
+ expect(mutedThings.threads).not.toContain(testEventId);
55
+ });
56
+ it("should remove an event from hidden tags in mute list", async () => {
57
+ // First add the thread to hidden mute list
58
+ const addSpy = subscribeSpyTo(hub.exec(MuteThread, testEventId, true), { expectErrors: false });
59
+ await addSpy.onComplete();
60
+ // Then unmute it
61
+ const spy = subscribeSpyTo(hub.exec(UnmuteThread, testEventId, true), { expectErrors: false });
62
+ await spy.onComplete();
63
+ const emittedEvent = spy.getLastValue();
64
+ const hiddenMutedThings = await getHiddenMutedThings(emittedEvent);
65
+ expect(hiddenMutedThings?.threads).not.toContain(testEventId);
66
+ });
67
+ });
@@ -0,0 +1,10 @@
1
+ import { Action } from "../action-hub.js";
2
+ /** An action that adds a relay to the 10006 blocked relays event */
3
+ export declare function AddBlockedRelay(relay: string | string[], hidden?: boolean): Action;
4
+ /** An action that removes a relay from the 10006 blocked relays event */
5
+ export declare function RemoveBlockedRelay(relay: string | string[], hidden?: boolean): Action;
6
+ /** Creates a new blocked relays event */
7
+ export declare function NewBlockedRelays(relays?: string[] | {
8
+ public?: string[];
9
+ hidden?: string[];
10
+ }): Action;
@@ -0,0 +1,48 @@
1
+ import { modifyHiddenTags, modifyPublicTags } from "applesauce-factory/operations/event";
2
+ import { addRelayTag, removeRelayTag } from "applesauce-factory/operations/tag/relay";
3
+ import { kinds } from "nostr-tools";
4
+ function getBlockedRelaysEvent(events, self) {
5
+ const event = events.getReplaceable(kinds.BlockedRelaysList, self);
6
+ if (!event)
7
+ throw new Error("Can't find blocked relays event");
8
+ return event;
9
+ }
10
+ /** An action that adds a relay to the 10006 blocked relays event */
11
+ export function AddBlockedRelay(relay, hidden = false) {
12
+ return async function* ({ events, factory, self }) {
13
+ const blocked = getBlockedRelaysEvent(events, self);
14
+ const operation = Array.isArray(relay) ? relay.map((r) => addRelayTag(r)) : addRelayTag(relay);
15
+ const draft = await factory.modifyTags(blocked, hidden ? { hidden: operation } : operation);
16
+ yield await factory.sign(draft);
17
+ };
18
+ }
19
+ /** An action that removes a relay from the 10006 blocked relays event */
20
+ export function RemoveBlockedRelay(relay, hidden = false) {
21
+ return async function* ({ events, factory, self }) {
22
+ const blocked = getBlockedRelaysEvent(events, self);
23
+ const operation = Array.isArray(relay) ? relay.map((r) => removeRelayTag(r)) : removeRelayTag(relay);
24
+ const draft = await factory.modifyTags(blocked, hidden ? { hidden: operation } : operation);
25
+ yield await factory.sign(draft);
26
+ };
27
+ }
28
+ /** Creates a new blocked relays event */
29
+ export function NewBlockedRelays(relays) {
30
+ return async function* ({ events, factory, self }) {
31
+ const blocked = events.getReplaceable(kinds.BlockedRelaysList, self);
32
+ if (blocked)
33
+ throw new Error("Blocked relays event already exists");
34
+ let publicOperations = [];
35
+ let hiddenOperations = [];
36
+ if (Array.isArray(relays)) {
37
+ publicOperations.push(...relays.map((r) => addRelayTag(r)));
38
+ }
39
+ else {
40
+ if (relays?.public)
41
+ publicOperations.push(...(relays?.public ?? []).map((r) => addRelayTag(r)));
42
+ if (relays?.hidden)
43
+ hiddenOperations.push(...(relays?.hidden ?? []).map((r) => addRelayTag(r)));
44
+ }
45
+ const draft = await factory.build({ kind: kinds.BlockedRelaysList }, publicOperations.length ? modifyPublicTags(...publicOperations) : undefined, hiddenOperations.length ? modifyHiddenTags(...hiddenOperations) : undefined);
46
+ yield await factory.sign(draft);
47
+ };
48
+ }
@@ -0,0 +1,9 @@
1
+ import { Action } from "../action-hub.js";
2
+ /** An action that adds a server to the Blossom servers event */
3
+ export declare function AddBlossomServer(server: string | URL | (string | URL)[]): Action;
4
+ /** An action that removes a server from the Blossom servers event */
5
+ export declare function RemoveBlossomServer(server: string | URL | (string | URL)[]): Action;
6
+ /** Makes a specific Blossom server the default server (move it to the top of the list) */
7
+ export declare function SetDefaultBlossomServer(server: string | URL): Action;
8
+ /** Creates a new Blossom servers event */
9
+ export declare function NewBlossomServers(servers?: (string | URL)[]): Action;