Skip to content

Commit 0ec5133

Browse files
committed
feat: add actor mute/unmute
1 parent 5d15ba6 commit 0ec5133

7 files changed

Lines changed: 194 additions & 11 deletions

File tree

docs/guide/features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ Trotsky is currently limited to the following features:
1111
**StepActorFollowings** | :white_check_mark: | Get an actor's followings | ```Trotsky.init(agent).actor('bsky.app').followings().each()```
1212
**StepActorLikes** | :white_check_mark: | Get an actor's likes. | ```Trotsky.init(agent).actor('bsky.app').likes().each()```
1313
**StepActorLists** | :white_check_mark: | Get an actor's lists. | ```Trotsky.init(agent).actor('bsky.app').lists().each()```
14-
**StepActorMute** | :x: | Mute an actor. |
14+
**StepActorMute** | :white_check_mark: | Mute an actor. | ```Trotsky.init(agent).actor('bsky.app').mute()```
1515
**StepActorPosts** | :white_check_mark: | Get an actor's posts | ```Trotsky.init(agent).actor('bsky.app').posts().each()```
1616
**StepActors** | :white_check_mark: | Get a list of actors by their DIDs or handles. | ```Trotsky.init(agent).actors(['bsky.app', 'trotsky.pirhoo.com']).each()```
1717
**StepActorStarterPacks** | :x: | Get an actor starter packs. |
1818
**StepActorStreamPosts** | :test_tube: | Stream an actor's posts. | ```Trotsky.init(agent).actor('bsky.app').streamPosts().each()```
1919
**StepActorUnblock** | :white_check_mark: | Unblock an actor. | ```Trotsky.init(agent).actor('bsky.app').unblock()```
2020
**StepActorUnfollow** | :white_check_mark: | Unfollow an actor. | ```Trotsky.init(agent).actor('bsky.app').unfollow()```
21-
**StepActorUnmute** | :x: | Unmute an actor. |
21+
**StepActorUnmute** | :white_check_mark: | Unmute an actor. | ```Trotsky.init(agent).actor('bsky.app').unmute()```
2222
**StepCreatePost** | :white_check_mark: | Create a post. | ```Trotsky.init(agent).post({ text: "Mapo Tofu is spicy 🌶️" })```
2323
**StepList** | :white_check_mark: | Get a list by its URI. | ```Trotsky.init(agent).list("at://did:plc:4cs4fudwvazeed2f4b6zjkj5/app.bsky.graph.list/3lbmn7qvjfr2m")```
2424
**StepListMembers** | :white_check_mark: | Get a list's members. | ```Trotsky.init(agent).list("at://did:plc:4cs4fudwvazeed2f4b6zjkj5/app.bsky.graph.list/3lbmn7qvjfr2m").members().each()```

lib/core/StepActorMute.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Step, type StepActor, type StepActorOutput } from "../trotsky"
2+
3+
/**
4+
* Represents a step that performs an actor mute operation using the Bluesky API.
5+
* @typeParam P - Type of the parent step, extending {@link StepActor}.
6+
* @typeParam C - Type of the context object, extending {@link StepActorOutput}.
7+
* @typeParam O - Type of the output object.
8+
* @public
9+
*/
10+
export class StepActorMute<P = StepActor, C extends StepActorOutput = StepActorOutput, O = null> extends Step<P, C, O> {
11+
12+
/**
13+
* Applies the step by performing a mute operation.
14+
* Requires the context to provide the `did` of the actor to mute.
15+
* @throws Error
16+
* if no context is found.
17+
*/
18+
async apply () {
19+
if (!this.context) {
20+
throw new Error("No context found for StepActorMute")
21+
}
22+
23+
const actor = this.context.did
24+
await this.agent.mute(actor)
25+
}
26+
}

lib/core/StepActorUnmute.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Step, type StepActor, type StepActorOutput } from "../trotsky"
2+
3+
/**
4+
* Represents step that unmutes the current actor (if muted).
5+
*
6+
* @typeParam P - The parent step type, defaulting to {@link StepActor}.
7+
* @typeParam C - The context or child output type, defaulting to {@link StepActorOutput}.
8+
* @typeParam O - The output type produced by this step, defaulting to `null`.
9+
* @public
10+
*/
11+
export class StepActorUnmute<P = StepActor, C extends StepActorOutput = StepActorOutput, O = null> extends Step<P, C, O> {
12+
13+
/**
14+
* Applies the unmute logic by calling the unmute API endpoint.
15+
*
16+
* @throws Error
17+
* If no context is available for the step, indicating the actor is unknown.
18+
*/
19+
async apply () {
20+
if (!this.context) {
21+
throw new Error("No context found for StepActorUnmute")
22+
}
23+
24+
const actor = this.context.did
25+
await this.agent.unmute(actor)
26+
}
27+
}

lib/core/mixins/ActorMixins.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import {
2-
Step,
3-
StepActorBlock,
4-
StepActorFollow,
5-
StepActorFollowers,
6-
StepActorFollowings,
2+
Step,
3+
StepActorBlock,
4+
StepActorFollow,
5+
StepActorFollowers,
6+
StepActorFollowings,
77
StepActorLists,
8-
StepActorLikes,
9-
StepActorPosts,
10-
StepActorUnblock,
8+
StepActorLikes,
9+
StepActorMute,
10+
StepActorPosts,
11+
StepActorUnblock,
1112
StepActorUnfollow,
13+
StepActorUnmute,
1214
StepActorStreamPosts
1315
} from "../../trotsky"
1416

@@ -111,11 +113,31 @@ export abstract class ActorMixins<P, C, O> extends Step<P, C, O> {
111113

112114
/**
113115
* Appends a step to unfollow the current actor.
114-
*
116+
*
115117
* @returns The current instance for method chaining.
116118
*/
117119
unfollow () {
118120
this.append(StepActorUnfollow<this>)
119121
return this
120122
}
123+
124+
/**
125+
* Appends a step to mute the current actor.
126+
*
127+
* @returns The current instance for method chaining.
128+
*/
129+
mute () {
130+
this.append(StepActorMute<this>)
131+
return this
132+
}
133+
134+
/**
135+
* Appends a step to unmute the current actor.
136+
*
137+
* @returns The current instance for method chaining.
138+
*/
139+
unmute () {
140+
this.append(StepActorUnmute<this>)
141+
return this
142+
}
121143
}

lib/trotsky.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,12 @@ export * from "./core/StepActorFollowers"
7878
export * from "./core/StepActorFollowings"
7979
export * from "./core/StepActorLikes"
8080
export * from "./core/StepActorLists"
81+
export * from "./core/StepActorMute"
8182
export * from "./core/StepActorPosts"
8283
export * from "./core/StepActorStreamPosts"
8384
export * from "./core/StepActorUnblock"
8485
export * from "./core/StepActorUnfollow"
86+
export * from "./core/StepActorUnmute"
8587

8688
// Single post
8789
export * from "./core/StepCreatePost"

tests/core/StepActorMute.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { afterAll, beforeAll, describe, expect, test } from "@jest/globals"
2+
import { AtpAgent } from "@atproto/api"
3+
import { TestNetwork, SeedClient, usersSeed } from "@atproto/dev-env"
4+
5+
import { Trotsky } from "../../lib/trotsky"
6+
7+
describe("StepActorMute", () => {
8+
let network: TestNetwork
9+
let agent: AtpAgent
10+
let sc: SeedClient
11+
let alice: { "did": string; "handle": string; "password": string }
12+
let bob: { "did": string; "handle": string; "password": string }
13+
14+
beforeAll(async () => {
15+
network = await TestNetwork.create({ "dbPostgresSchema": "step_actor_mute" })
16+
agent = network.pds.getClient()
17+
18+
sc = network.getSeedClient()
19+
await usersSeed(sc)
20+
21+
bob = sc.accounts[sc.dids.bob]
22+
alice = sc.accounts[sc.dids.alice]
23+
24+
await network.processAll()
25+
await agent.login({ "identifier": bob.handle, "password": bob.password })
26+
})
27+
28+
afterAll(async () => {
29+
await network.close()
30+
})
31+
32+
test("mute Alice", async () => {
33+
await Trotsky.init(agent).actor(alice.handle).mute().wait(1e3).run()
34+
const { "data": { mutes } } = await agent.app.bsky.graph.getMutes()
35+
36+
expect(mutes).toEqual(
37+
expect.arrayContaining([
38+
expect.objectContaining({
39+
"handle": alice.handle
40+
})
41+
])
42+
)
43+
})
44+
})

tests/core/StepActorUnmute.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { afterAll, beforeAll, describe, expect, test } from "@jest/globals"
2+
import { AtpAgent } from "@atproto/api"
3+
import { TestNetwork, SeedClient, usersSeed } from "@atproto/dev-env"
4+
5+
import { Trotsky } from "../../lib/trotsky"
6+
7+
describe("StepActorUnmute", () => {
8+
let network: TestNetwork
9+
let agent: AtpAgent
10+
let sc: SeedClient
11+
let alice: { "did": string; "handle": string; "password": string }
12+
let bob: { "did": string; "handle": string; "password": string }
13+
let carol: { "did": string; "handle": string; "password": string }
14+
15+
beforeAll(async () => {
16+
network = await TestNetwork.create({ "dbPostgresSchema": "step_actor_unmute" })
17+
agent = network.pds.getClient()
18+
19+
sc = network.getSeedClient()
20+
await usersSeed(sc)
21+
22+
bob = sc.accounts[sc.dids.bob]
23+
alice = sc.accounts[sc.dids.alice]
24+
carol = sc.accounts[sc.dids.carol]
25+
26+
await network.processAll()
27+
await agent.login({ "identifier": bob.handle, "password": bob.password })
28+
})
29+
30+
afterAll(async () => {
31+
await network.close()
32+
})
33+
34+
test("unmute Alice", async () => {
35+
await Trotsky.init(agent).actor(alice.handle).mute().wait(1e3).run()
36+
await Trotsky.init(agent).actor(alice.handle).unmute().wait(1e3).run()
37+
38+
const { "data": { mutes } } = await agent.app.bsky.graph.getMutes()
39+
40+
expect(mutes).not.toEqual(
41+
expect.arrayContaining([
42+
expect.objectContaining({
43+
"handle": alice.handle
44+
})
45+
])
46+
)
47+
})
48+
49+
test("unmute Carol does nothing even if she is not muted", async () => {
50+
await Trotsky.init(agent).actor(carol.handle).unmute().wait(1e3).run()
51+
52+
const { "data": { mutes } } = await agent.app.bsky.graph.getMutes()
53+
54+
expect(mutes).not.toEqual(
55+
expect.arrayContaining([
56+
expect.objectContaining({
57+
"handle": carol.handle
58+
})
59+
])
60+
)
61+
})
62+
})

0 commit comments

Comments
 (0)