Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: follow on semble quick action

Hugo 8e2b1fbd c93f11b6

+66 -7
+1
app/components/Favicon/index.tsx
··· 9 9 "exosphere.site": "/static/favicons/exosphere.site.cf36edf8.svg", 10 10 "margin.at": "/static/favicons/margin.at.cf36edf8.svg", 11 11 "sifa.id": "/static/favicons/sifa.id.f49fecdf.svg", 12 + "semble.so": "/static/favicons/semble.so.27ea0d97.svg", 12 13 }; 13 14 14 15 export function Favicon({ domain, class: className }: { domain: string; class?: string }) {
+1 -1
app/islands/AutomationForm.tsx
··· 1204 1204 const target = type.slice("follow-".length) as FollowTarget; 1205 1205 setActions((prev) => [ 1206 1206 ...prev, 1207 - { type: "follow", target, subject: "{{event.did}}", comment: "" }, 1207 + { type: "follow", target, subject: "{{event.commit.record.subject}}", comment: "" }, 1208 1208 ]); 1209 1209 } else { 1210 1210 setActions((prev) => [
+4
app/styles/action-header.css.ts
··· 38 38 backgroundColor: vars.color.appsSubtle, 39 39 color: vars.color.apps, 40 40 }, 41 + '&[data-cat="cosmik"]': { 42 + backgroundColor: vars.color.cosmikSubtle, 43 + color: vars.color.cosmik, 44 + }, 41 45 '&[data-cat="sifa"]': { 42 46 backgroundColor: vars.color.sifaSubtle, 43 47 color: vars.color.sifa,
+4
app/styles/theme.css.ts
··· 34 34 pdsSubtle: "color-pds-subtle", 35 35 apps: "color-apps", 36 36 appsSubtle: "color-apps-subtle", 37 + cosmik: "color-cosmik", 38 + cosmikSubtle: "color-cosmik-subtle", 37 39 sifa: "color-sifa", 38 40 sifaSubtle: "color-sifa-subtle", 39 41 tangled: "color-tangled", ··· 104 106 [vars.color.pdsSubtle]: darkColors.pdsSubtle, 105 107 [vars.color.apps]: darkColors.apps, 106 108 [vars.color.appsSubtle]: darkColors.appsSubtle, 109 + [vars.color.cosmik]: darkColors.cosmik, 110 + [vars.color.cosmikSubtle]: darkColors.cosmikSubtle, 107 111 [vars.color.sifa]: darkColors.sifa, 108 112 [vars.color.sifaSubtle]: darkColors.sifaSubtle, 109 113 [vars.color.tangled]: darkColors.tangled,
+6
app/styles/tokens/colors.ts
··· 34 34 pdsSubtle: "oklch(0.22 0.04 300)", 35 35 apps: "oklch(0.65 0.22 262)", 36 36 appsSubtle: "oklch(0.22 0.05 262)", 37 + // Semble's brand orange (#FF6400) at hue 45, kept clear of the amber accent 38 + // (hue 65) so a Semble tile next to an accent button stays distinguishable. 39 + cosmik: "oklch(0.75 0.18 45)", 40 + cosmikSubtle: "oklch(0.22 0.05 45)", 37 41 // Hue shifted toward 220 and chroma nudged so dark-mode Sifa reads distinctly 38 42 // from Bluesky (245) when both tiles appear together. 39 43 sifa: "oklch(0.68 0.14 220)", ··· 78 82 pdsSubtle: "oklch(0.96 0.03 300)", 79 83 apps: "#2563EB", 80 84 appsSubtle: "oklch(0.95 0.04 262)", 85 + cosmik: "#FF6400", 86 + cosmikSubtle: "oklch(0.95 0.05 45)", 81 87 sifa: "#4385BE", 82 88 sifaSubtle: "oklch(0.96 0.03 220)", 83 89 tangled: "oklch(0.30 0 0)",
+2 -2
lexicons/run/airglow/automation.json
··· 344 344 }, 345 345 "followAction": { 346 346 "type": "object", 347 - "description": "Follow a subject DID on an AT Protocol social graph (Bluesky, Tangled, or Sifa). All three produce records with the same shape; only the collection NSID differs.", 347 + "description": "Follow a subject DID on an AT Protocol social graph (Bluesky, Tangled, Sifa, or Semble). All produce records with the same shape; only the collection NSID differs.", 348 348 "required": ["target", "subject"], 349 349 "properties": { 350 350 "target": { 351 351 "type": "string", 352 352 "description": "Which social graph to follow on.", 353 - "knownValues": ["bluesky", "tangled", "sifa"], 353 + "knownValues": ["bluesky", "cosmik", "sifa", "tangled"], 354 354 "maxLength": 32 355 355 }, 356 356 "subject": {
+16 -1
lib/actions/follow.test.ts
··· 96 96 expect(logs[0]!.statusCode).toBe(200); 97 97 }); 98 98 99 - it("maps tangled and sifa targets to their collections", async () => { 99 + it("maps tangled, sifa, and cosmik targets to their collections", async () => { 100 100 mockCreateRecord.mockResolvedValue({ uri: "at://x/_/rk", cid: "c" }); 101 101 102 102 const tangled = makeFollowAction({ ··· 109 109 const sifa = makeFollowAction({ target: "sifa", subject: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }); 110 110 await executeFollow(makeMatch({ automation: { actions: [sifa] } }), 0); 111 111 expect(mockCreateRecord.mock.calls[1]![1]).toBe("id.sifa.graph.follow"); 112 + 113 + const cosmik = makeFollowAction({ 114 + target: "cosmik", 115 + subject: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa", 116 + }); 117 + await executeFollow(makeMatch({ automation: { actions: [cosmik] } }), 0); 118 + expect(mockCreateRecord.mock.calls[2]![1]).toBe("network.cosmik.follow"); 112 119 }); 113 120 114 121 it("fails without retry when subject renders to an empty string", async () => { ··· 246 253 const sifa = makeFollowAction({ target: "sifa", subject: EVENT_DID }); 247 254 await executeFollow(makeMatch({ automation: { actions: [sifa] } }), 0); 248 255 expect(mockFetchRecord.mock.calls[1]![0]).toBe(`at://${EVENT_DID}/id.sifa.profile.self/self`); 256 + 257 + // Semble re-uses the Bluesky profile lexicon, so the cosmik pre-check 258 + // hits app.bsky.actor.profile/self rather than a Cosmik-specific NSID. 259 + const cosmik = makeFollowAction({ target: "cosmik", subject: EVENT_DID }); 260 + await executeFollow(makeMatch({ automation: { actions: [cosmik] } }), 0); 261 + expect(mockFetchRecord.mock.calls[2]![0]).toBe( 262 + `at://${EVENT_DID}/app.bsky.actor.profile/self`, 263 + ); 249 264 }); 250 265 251 266 it("proceeds with the follow when a profile lookup throws (fail-open)", async () => {
+11 -2
lib/automations/action-catalogue.test.ts
··· 12 12 expect(bsky.actions.map((a) => a.id)).toEqual(["bsky-post", "follow-bluesky", "bsky-like"]); 13 13 }); 14 14 15 - it("orders Apps tiles: Bookmark → Follow Sifa → Follow Tangled", () => { 15 + it("orders Apps tiles: Bookmark → Follow Semble → Follow Sifa → Follow Tangled", () => { 16 16 const apps = ACTION_CATALOGUE.find((c) => c.id === "apps")!; 17 - expect(apps.actions.map((a) => a.id)).toEqual(["bookmark", "follow-sifa", "follow-tangled"]); 17 + expect(apps.actions.map((a) => a.id)).toEqual([ 18 + "bookmark", 19 + "follow-cosmik", 20 + "follow-sifa", 21 + "follow-tangled", 22 + ]); 18 23 }); 19 24 20 25 it("groups follow tiles under their parent category (not a dedicated follow group)", () => { 21 26 const categoryOf = (id: string) => 22 27 ACTION_CATALOGUE.find((c) => c.actions.some((a) => a.id === id))?.id; 23 28 expect(categoryOf("follow-bluesky")).toBe("bluesky"); 29 + expect(categoryOf("follow-cosmik")).toBe("apps"); 24 30 expect(categoryOf("follow-sifa")).toBe("apps"); 25 31 expect(categoryOf("follow-tangled")).toBe("apps"); 26 32 }); ··· 31 37 expect(ACTION_INFO_BY_TYPE["follow-sifa"]!.colorKey).toBe("sifa"); 32 38 expect(ACTION_INFO_BY_TYPE["follow-tangled"]!.colorKey).toBe("tangled"); 33 39 expect(ACTION_INFO_BY_TYPE["follow-bluesky"]!.colorKey).toBe("bluesky"); 40 + expect(ACTION_INFO_BY_TYPE["follow-cosmik"]!.colorKey).toBe("cosmik"); 34 41 }); 35 42 36 43 it("leaves colorKey undefined for tiles that use their category's color", () => { ··· 43 50 expect(ACTION_INFO_BY_TYPE["follow-bluesky"]!.faviconDomain).toBe("bsky.app"); 44 51 expect(ACTION_INFO_BY_TYPE["follow-sifa"]!.faviconDomain).toBe("sifa.id"); 45 52 expect(ACTION_INFO_BY_TYPE["follow-tangled"]!.faviconDomain).toBe("tangled.sh"); 53 + expect(ACTION_INFO_BY_TYPE["follow-cosmik"]!.faviconDomain).toBe("semble.so"); 46 54 expect(ACTION_INFO_BY_TYPE["bookmark"]!.faviconDomain).toBe("margin.at"); 47 55 }); 48 56 }); ··· 50 58 describe("actionTypeKey", () => { 51 59 it("maps follow actions to follow-<target> tiles", () => { 52 60 expect(actionTypeKey({ $type: "follow", target: "bluesky" })).toBe("follow-bluesky"); 61 + expect(actionTypeKey({ $type: "follow", target: "cosmik" })).toBe("follow-cosmik"); 53 62 expect(actionTypeKey({ $type: "follow", target: "sifa" })).toBe("follow-sifa"); 54 63 expect(actionTypeKey({ $type: "follow", target: "tangled" })).toBe("follow-tangled"); 55 64 });
+15 -1
lib/automations/follow-targets.ts
··· 1 1 /** Keys that map to a CSS selector in action-header.css.ts. Keep in sync 2 2 * with the `&[data-cat="..."]` selectors there; a typo silently loses the 3 3 * accent otherwise. */ 4 - export type ColorKey = "webhook" | "bluesky" | "pds" | "apps" | "sifa" | "tangled"; 4 + export type ColorKey = "webhook" | "bluesky" | "pds" | "apps" | "sifa" | "tangled" | "cosmik"; 5 5 6 6 type FollowTargetEntry = { 7 7 /** Catalogue category the tile lives under. */ ··· 45 45 description: "Follow someone on Bluesky", 46 46 faviconDomain: "bsky.app", 47 47 collection: "app.bsky.graph.follow", 48 + profileCollection: "app.bsky.actor.profile", 49 + profileRkey: "self", 50 + }, 51 + cosmik: { 52 + catId: "apps", 53 + colorKey: "cosmik", 54 + appName: "Semble", 55 + label: "Follow on Semble", 56 + description: "Follow someone on Semble", 57 + faviconDomain: "semble.so", 58 + collection: "network.cosmik.follow", 59 + // Semble (the Cosmik network) does not ship its own profile lexicon, so 60 + // it re-uses the Bluesky profile record. The pre-check points at 61 + // `app.bsky.actor.profile/self`, same as the bluesky target. 48 62 profileCollection: "app.bsky.actor.profile", 49 63 profileRkey: "self", 50 64 },
+6
public/static/favicons/semble.so.27ea0d97.svg
··· 1 + <svg width="32" height="43" viewBox="0 0 32 43" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M31.0164 33.1306C31.0164 38.581 25.7882 42.9994 15.8607 42.9994C5.93311 42.9994 0 37.5236 0 32.0732C0 26.6228 5.93311 23.2617 15.8607 23.2617C25.7882 23.2617 31.0164 27.6802 31.0164 33.1306Z" fill="#FF6400"/> 3 + <path d="M25.7295 19.3862C25.7295 22.5007 20.7964 22.2058 15.1558 22.2058C9.51511 22.2058 4.93445 22.1482 4.93445 19.0337C4.93445 15.9192 9.71537 12.6895 15.356 12.6895C20.9967 12.6895 25.7295 16.2717 25.7295 19.3862Z" fill="#FF6400"/> 4 + <path d="M25.0246 10.9256C25.0246 14.0401 20.7964 11.9829 15.1557 11.9829C9.51506 11.9829 6.34424 13.6876 6.34424 10.5731C6.34424 7.45857 9.51506 5.63867 15.1557 5.63867C20.7964 5.63867 25.0246 7.81103 25.0246 10.9256Z" fill="#FF6400"/> 5 + <path d="M20.4426 3.5755C20.4426 5.8323 18.2088 4.22951 15.2288 4.22951C12.2489 4.22951 10.5737 5.8323 10.5737 3.5755C10.5737 1.31871 12.2489 0 15.2288 0C18.2088 0 20.4426 1.31871 20.4426 3.5755Z" fill="#FF6400"/> 6 + </svg>