Skip to content

Commit 408adcd

Browse files
authored
Add Slack pins and bookmarks parity (#161)
* Add Slack pins and bookmarks parity - Add stateful pins and link bookmark APIs with scope checks and inspector visibility - Cover route, SDK, event, coverage matrix, docs, and skill updates * Fix Slack orphaned pins * Fix Slack bookmark ordering Sort bookmarks by rank with stable fallbacks and avoid rank reuse after deletion. * Fix Slack seeded token identity handling
1 parent 4dac8fb commit 408adcd

19 files changed

Lines changed: 1386 additions & 43 deletions

File tree

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,13 @@ slack:
265265
name: My Slack App
266266
redirect_uris:
267267
- http://localhost:3000/api/auth/callback/slack
268-
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write, files:read, files:write]
268+
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write, files:read, files:write, pins:read, pins:write, bookmarks:read, bookmarks:write]
269269
user_scopes: [users:read, users.profile:read]
270270
bot_name: my-bot
271271
tokens:
272272
- token: xoxb-local-test
273273
user: developer
274-
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write, files:read, files:write]
274+
scopes: [chat:write, channels:read, users.profile:read, users.profile:write, users:write, files:read, files:write, pins:read, pins:write, bookmarks:read, bookmarks:write]
275275
strict_scopes: false
276276

277277
apple:
@@ -612,7 +612,7 @@ OAuth 2.0, OpenID Connect, and mutable Google Workspace-style surfaces for local
612612

613613
## Slack API
614614

615-
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
615+
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, pins, bookmarks, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Pin and bookmark writes support channel message pins and link bookmarks. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
616616

617617
### Auth & Chat
618618
- `POST /api/auth.test` - test authentication
@@ -660,6 +660,15 @@ Fully stateful Slack Web API emulation with channels, messages, threads, reactio
660660
- `GET /files-pri/:fileId/:filename` - download file bytes with a bearer token that can access the file
661661
- `POST /api/files.delete` - delete a completed file
662662

663+
### Pins & Bookmarks
664+
- `POST /api/pins.add` - pin a message to a channel
665+
- `GET /api/pins.list` / `POST /api/pins.list` - list pinned message items for a channel
666+
- `POST /api/pins.remove` - remove a message pin from a channel
667+
- `POST /api/bookmarks.add` - add a link bookmark to a channel
668+
- `POST /api/bookmarks.edit` - update a link bookmark
669+
- `POST /api/bookmarks.list` - list channel bookmarks
670+
- `POST /api/bookmarks.remove` - remove a bookmark from a channel
671+
663672
### Team, Bots & Webhooks
664673
- `POST /api/team.info` - workspace info
665674
- `POST /api/bots.info` - bot info
@@ -669,7 +678,7 @@ Fully stateful Slack Web API emulation with channels, messages, threads, reactio
669678
- `GET /oauth/v2/authorize` - authorization (shows user picker)
670679
- `POST /api/oauth.v2.access` - token exchange
671680

672-
Slack scope checks are relaxed by default so local tests can use simple bearer tokens. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user, presence, and file checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, and `files:write`.
681+
Slack scope checks are relaxed by default so local tests can use simple bearer tokens. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user, presence, file, pin, and bookmark checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, `files:write`, `pins:read`, `pins:write`, `bookmarks:read`, and `bookmarks:write`.
673682

674683
## Apple Sign In
675684

apps/web/app/docs/configuration/page.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ slack:
224224
- users:write
225225
- files:read
226226
- files:write
227+
- pins:read
228+
- pins:write
229+
- bookmarks:read
230+
- bookmarks:write
227231
tokens:
228232
- token: xoxb-local-test
229233
user: developer
@@ -235,6 +239,10 @@ slack:
235239
- users:write
236240
- files:read
237241
- files:write
242+
- pins:read
243+
- pins:write
244+
- bookmarks:read
245+
- bookmarks:write
238246
incoming_webhooks:
239247
- channel: general
240248
label: CI Notifications

apps/web/app/docs/slack/page.mdx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Slack API
22

3-
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records. State changes dispatch `event_callback` payloads to configured webhook URLs.
3+
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, pins, bookmarks, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Pin and bookmark writes support channel message pins and link bookmarks. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records. State changes dispatch `event_callback` payloads to configured webhook URLs.
44

55
## Auth
66

77
- `POST /api/auth.test` - test authentication
88

9-
Scope checks are relaxed by default for local development. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user, presence, and file checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, and `files:write`.
9+
Scope checks are relaxed by default for local development. Set `slack.strict_scopes: true` in seed config to make supported Web API methods return Slack-style `missing_scope` errors with `needed` and `provided` fields. Supported user, presence, file, pin, and bookmark checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, `files:write`, `pins:read`, `pins:write`, `bookmarks:read`, and `bookmarks:write`.
1010

1111
## Chat
1212

@@ -67,6 +67,16 @@ Scope checks are relaxed by default for local development. Set `slack.strict_sco
6767
- `GET /files-pri/:fileId/:filename` - download file bytes with a bearer token that can access the file
6868
- `POST /api/files.delete` - delete a completed file
6969

70+
## Pins And Bookmarks
71+
72+
- `POST /api/pins.add` - pin a message to a channel
73+
- `GET /api/pins.list` / `POST /api/pins.list` - list pinned message items for a channel
74+
- `POST /api/pins.remove` - remove a message pin from a channel
75+
- `POST /api/bookmarks.add` - add a link bookmark to a channel
76+
- `POST /api/bookmarks.edit` - update a link bookmark
77+
- `POST /api/bookmarks.list` - list channel bookmarks
78+
- `POST /api/bookmarks.remove` - remove a bookmark from a channel
79+
7080
## Team
7181

7282
- `POST /api/team.info` - get workspace info
@@ -88,13 +98,14 @@ Scope checks are relaxed by default for local development. Set `slack.strict_sco
8898

8999
## Event Dispatching
90100

91-
When messages are posted, updated, deleted, or reactions change, the emulator dispatches `event_callback` payloads to configured webhook URLs matching Slack's Events API format:
101+
When messages are posted, updated, deleted, reactions change, pins change, or files change, the emulator dispatches `event_callback` payloads to configured webhook URLs matching Slack's Events API format:
92102

93103
- `message` events on `chat.postMessage`
94104
- `message` with `subtype: message_changed` on `chat.update`
95105
- `message` with `subtype: message_deleted` on `chat.delete`
96106
- rich message fields are included on posted `message` events when present
97107
- `reaction_added` / `reaction_removed` events on `reactions.add` / `reactions.remove`
108+
- `pin_added` / `pin_removed` events on `pins.add` / `pins.remove`
98109
- `message` with `subtype: bot_message` on incoming webhook posts
99110
- `channel_archive` / `channel_unarchive` for public lifecycle archive writes
100111
- `group_archive` / `group_unarchive` for private lifecycle archive writes

packages/@emulators/slack/README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @emulators/slack
22

3-
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
3+
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, pins, bookmarks, OAuth v2, and incoming webhooks. Chat writes preserve common rich message fields such as `blocks`, `attachments`, `metadata`, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Pin and bookmark writes support channel message pins and link bookmarks. OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records.
44

55
Part of [emulate](https://github.com/vercel-labs/emulate) — local drop-in replacement services for CI and no-network sandboxes.
66

@@ -58,6 +58,15 @@ npm install @emulators/slack
5858
- `GET /files-pri/:fileId/:filename` — download file bytes with a bearer token that can access the file
5959
- `POST /api/files.delete` — delete a completed file
6060

61+
### Pins & Bookmarks
62+
- `POST /api/pins.add` — pin a message to a channel
63+
- `GET /api/pins.list` / `POST /api/pins.list` — list pinned message items for a channel
64+
- `POST /api/pins.remove` — remove a message pin from a channel
65+
- `POST /api/bookmarks.add` — add a link bookmark to a channel
66+
- `POST /api/bookmarks.edit` — update a link bookmark
67+
- `POST /api/bookmarks.list` — list channel bookmarks
68+
- `POST /api/bookmarks.remove` — remove a bookmark from a channel
69+
6170
### Team, Bots & Webhooks
6271
- `POST /api/team.info` — workspace info
6372
- `POST /api/bots.info` — bot info
@@ -69,7 +78,7 @@ npm install @emulators/slack
6978

7079
## Auth
7180

72-
All Web API endpoints require `Authorization: Bearer <token>`. Seeded OAuth apps create local installation state, and the OAuth v2 flow with user picker UI returns Slack-style bot tokens. Scope checks are relaxed by default for local development. Set `strict_scopes: true` in Slack seed config to return Slack-style `missing_scope` errors when a token lacks the required method scope. Supported user, presence, and file checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, and `files:write`.
81+
All Web API endpoints require `Authorization: Bearer <token>`. Seeded OAuth apps create local installation state, and the OAuth v2 flow with user picker UI returns Slack-style bot tokens. Scope checks are relaxed by default for local development. Set `strict_scopes: true` in Slack seed config to return Slack-style `missing_scope` errors when a token lacks the required method scope. Supported user, presence, file, pin, and bookmark checks include `users:read`, `users:read.email`, `users.profile:read`, `users.profile:write`, `users:write`, `files:read`, `files:write`, `pins:read`, `pins:write`, `bookmarks:read`, and `bookmarks:write`.
7382

7483
## Seed Configuration
7584

@@ -109,6 +118,10 @@ slack:
109118
- users:write
110119
- files:read
111120
- files:write
121+
- pins:read
122+
- pins:write
123+
- bookmarks:read
124+
- bookmarks:write
112125
user_scopes:
113126
- users:read
114127
- users.profile:read
@@ -124,6 +137,10 @@ slack:
124137
- users:write
125138
- files:read
126139
- files:write
140+
- pins:read
141+
- pins:write
142+
- bookmarks:read
143+
- bookmarks:write
127144
strict_scopes: false
128145
```
129146

packages/@emulators/slack/src/__tests__/slack-coverage.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,62 @@ export const slackCoverageMatrix: SlackCoverageEntry[] = [
323323
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
324324
notes: "Returns reactions for message items only.",
325325
},
326+
{
327+
family: "pins",
328+
method: "pins.add",
329+
route: "POST /api/pins.add",
330+
status: "partial",
331+
testedBy: ["slack.test.ts", "slack-sdk.test.ts", "slack-events.test.ts"],
332+
notes: "Pins message items to channels and dispatches pin_added events.",
333+
},
334+
{
335+
family: "pins",
336+
method: "pins.list",
337+
route: ["GET /api/pins.list", "POST /api/pins.list"],
338+
status: "partial",
339+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
340+
notes: "Lists pinned message items for a channel.",
341+
},
342+
{
343+
family: "pins",
344+
method: "pins.remove",
345+
route: "POST /api/pins.remove",
346+
status: "partial",
347+
testedBy: ["slack.test.ts", "slack-sdk.test.ts", "slack-events.test.ts"],
348+
notes: "Removes message pins from channels and dispatches pin_removed events.",
349+
},
350+
{
351+
family: "bookmarks",
352+
method: "bookmarks.add",
353+
route: "POST /api/bookmarks.add",
354+
status: "partial",
355+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
356+
notes: "Adds link bookmarks to channels.",
357+
},
358+
{
359+
family: "bookmarks",
360+
method: "bookmarks.edit",
361+
route: "POST /api/bookmarks.edit",
362+
status: "partial",
363+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
364+
notes: "Updates local link bookmark title, link, and emoji fields.",
365+
},
366+
{
367+
family: "bookmarks",
368+
method: "bookmarks.list",
369+
route: "POST /api/bookmarks.list",
370+
status: "partial",
371+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
372+
notes: "Lists local link bookmarks for a channel.",
373+
},
374+
{
375+
family: "bookmarks",
376+
method: "bookmarks.remove",
377+
route: "POST /api/bookmarks.remove",
378+
status: "partial",
379+
testedBy: ["slack.test.ts", "slack-sdk.test.ts"],
380+
notes: "Removes local link bookmarks from channels.",
381+
},
326382
{
327383
family: "team",
328384
method: "team.info",
@@ -378,7 +434,7 @@ export const slackCoverageMatrix: SlackCoverageEntry[] = [
378434
route: "GET /",
379435
status: "partial",
380436
testedBy: ["slack.test.ts"],
381-
notes: "Shows channels and recent messages.",
437+
notes: "Shows channels, recent messages, scheduled messages, ephemeral messages, pins, and bookmarks.",
382438
},
383439
{
384440
family: "files",

packages/@emulators/slack/src/__tests__/slack-events.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,60 @@ describe("Slack plugin - event dispatch baseline", () => {
197197
]);
198198
});
199199

200+
it("dispatches pin add and remove events", async () => {
201+
const { app, store, webhooks } = createSlackTestApp();
202+
const capture = captureFetchRequests();
203+
registerSlackEventSubscription(webhooks, ["pin_added", "pin_removed"]);
204+
205+
const ch = getSlackStore(store).channels.findOneBy("name", "general")!;
206+
const postRes = await app.request(`${base}/api/chat.postMessage`, {
207+
method: "POST",
208+
headers: authHeaders(),
209+
body: JSON.stringify({ channel: ch.channel_id, text: "pin event baseline" }),
210+
});
211+
const posted = (await postRes.json()) as { ts: string };
212+
213+
await app.request(`${base}/api/pins.add`, {
214+
method: "POST",
215+
headers: authHeaders(),
216+
body: JSON.stringify({ channel: ch.channel_id, timestamp: posted.ts }),
217+
});
218+
219+
await app.request(`${base}/api/pins.remove`, {
220+
method: "POST",
221+
headers: authHeaders(),
222+
body: JSON.stringify({ channel: ch.channel_id, timestamp: posted.ts }),
223+
});
224+
225+
expect(capture.requests).toHaveLength(2);
226+
expect(capture.jsonBodies()).toEqual([
227+
expect.objectContaining({
228+
event: expect.objectContaining({
229+
type: "pin_added",
230+
user: "U000000001",
231+
channel_id: ch.channel_id,
232+
item: expect.objectContaining({
233+
type: "message",
234+
channel: ch.channel_id,
235+
message: expect.objectContaining({ text: "pin event baseline", pinned_to: [ch.channel_id] }),
236+
}),
237+
}),
238+
}),
239+
expect.objectContaining({
240+
event: expect.objectContaining({
241+
type: "pin_removed",
242+
user: "U000000001",
243+
channel_id: ch.channel_id,
244+
has_pins: false,
245+
item: expect.objectContaining({
246+
type: "message",
247+
channel: ch.channel_id,
248+
}),
249+
}),
250+
}),
251+
]);
252+
});
253+
200254
it("dispatches user_change events for profile writes", async () => {
201255
const { app, webhooks } = createSlackTestApp();
202256
const capture = captureFetchRequests();

0 commit comments

Comments
 (0)