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.

refactor: derive isRecordProducingAction, labels, and catalogue from registry

Hugo 0f65e0b4 96251806

+105 -123
+2 -1
app/components/LexiconFlow/index.tsx
··· 1 1 import { ArrowRight, Webhook } from "../../icons.ts"; 2 2 import { nsidToDomain } from "../../../lib/lexicons/resolver.ts"; 3 - import { isRecordProducingAction, type Action } from "../../../lib/db/schema.ts"; 3 + import { type Action } from "../../../lib/db/schema.ts"; 4 + import { isRecordProducingAction } from "../../../lib/actions/registry.ts"; 4 5 import { FOLLOW_TARGETS } from "../../../lib/automations/follow-targets.ts"; 5 6 import { Favicon, NsidCode } from "../NsidCode/index.tsx"; 6 7 import * as s from "./styles.css.ts";
+2 -6
app/islands/AutomationForm.tsx
··· 1 1 import { useState, useCallback, useRef, useMemo, useEffect } from "hono/jsx"; 2 2 import type { RecordSchema, SchemaNode } from "../../lib/lexicons/schema-types.js"; 3 3 import { nsidRequiresWantedDids } from "../../lib/lexicons/match.js"; 4 - import { 5 - isRecordProducingAction, 6 - type Action, 7 - type FetchStep, 8 - type FollowTarget, 9 - } from "../../lib/db/schema.js"; 4 + import { type Action, type FetchStep, type FollowTarget } from "../../lib/db/schema.js"; 5 + import { isRecordProducingAction } from "../../lib/actions/registry.js"; 10 6 import { 11 7 ACTION_CATALOGUE, 12 8 actionTypeKey,
+1
lib/actions/bsky-post.ts
··· 178 178 > = { 179 179 type: "bsky-post", 180 180 pdsType: "run.airglow.automation#bskyPostAction", 181 + displayLabel: "Bluesky Post", 181 182 recordProducing: true, 182 183 needsFullScope: true, 183 184 validate,
+1
lib/actions/executor.ts
··· 130 130 export const recordDefinition: ActionDefinition<RecordAction, RecordInput, PdsRecordAction> = { 131 131 type: "record", 132 132 pdsType: "run.airglow.automation#recordAction", 133 + displayLabel: "Create Record", 133 134 recordProducing: true, 134 135 needsFullScope: true, 135 136 validate,
+1
lib/actions/follow.ts
··· 252 252 export const followDefinition: ActionDefinition<FollowAction, FollowInput, PdsFollowAction> = { 253 253 type: "follow", 254 254 pdsType: "run.airglow.automation#followAction", 255 + displayLabel: "Follow", 255 256 recordProducing: true, 256 257 needsFullScope: true, 257 258 validate,
+1
lib/actions/margin-bookmark.ts
··· 323 323 > = { 324 324 type: "margin-bookmark", 325 325 pdsType: "run.airglow.automation#marginBookmarkAction", 326 + displayLabel: "Bookmark on Margin", 326 327 recordProducing: true, 327 328 needsFullScope: true, 328 329 validate,
+1
lib/actions/patch-record.ts
··· 208 208 > = { 209 209 type: "patch-record", 210 210 pdsType: "run.airglow.automation#patchRecordAction", 211 + displayLabel: "Update Record", 211 212 recordProducing: true, 212 213 needsFullScope: true, 213 214 validate,
+20 -3
lib/actions/registry.ts
··· 1 + import type { FC } from "hono/jsx"; 1 2 import type { Action } from "../db/schema.js"; 2 3 import type { PdsAction } from "../automations/pds.js"; 3 4 import type { ColorKey } from "../automations/follow-targets.js"; 5 + 6 + /** Common prop signature for the lucide-style icon components in `app/icons.ts`. */ 7 + type IconProps = { size?: number; class?: string; color?: string; "stroke-width"?: number }; 8 + export type ActionIcon = FC<IconProps>; 4 9 import type { 5 10 ActionHandler, 6 11 DryRunContext, ··· 17 22 * ACTION_CATALOGUE array in action-catalogue.ts becomes a derived view in 18 23 * Phase 4. */ 19 24 export type CatalogueTile = { 25 + /** Imperative phrase shown in the "add action" picker (e.g. "Post to Bluesky"). */ 20 26 label: string; 21 27 description: string; 22 28 category: "webhook" | "bluesky" | "apps" | "pds"; 23 - /** Lucide icon ref. Type left loose because the icon module is itself 24 - * loosely typed and we don't want to force a pre-Phase-4 import here. */ 25 - icon: unknown; 29 + /** Lucide-style icon component (one of the exports from `app/icons.ts`). */ 30 + icon: ActionIcon; 26 31 available: boolean; 27 32 /** Override for the tile's data-cat color (e.g. follow-sifa is sifa-blue 28 33 * while still living under "Bluesky"). */ ··· 50 55 > = { 51 56 type: TAction["$type"]; 52 57 pdsType: TPdsAction["$type"]; 58 + 59 + /** Short noun-phrase label used in the dashboard automation list and 60 + * delivery-log filters (e.g. "Bluesky Post", "Webhook"). Distinct from 61 + * `catalogue.label`, which is the imperative phrase in the picker. */ 62 + displayLabel: string; 53 63 54 64 /** Replaces `RECORD_PRODUCING_TYPES` and the `actionResultNames.push` calls 55 65 * in API routes. */ ··· 120 130 record: recordDefinition, 121 131 webhook: webhookDefinition, 122 132 }; 133 + 134 + /** Action types that produce a record result (uri, cid, rkey) for chaining. 135 + * Lookup goes through the registry — adding a new action with 136 + * `recordProducing: true` is enough; no parallel set to maintain. */ 137 + export function isRecordProducingAction(type: string): boolean { 138 + return ACTION_REGISTRY[type as ActionType]?.recordProducing ?? false; 139 + }
+1
lib/actions/semble-save.ts
··· 202 202 > = { 203 203 type: "semble-save", 204 204 pdsType: "run.airglow.automation#sembleSaveAction", 205 + displayLabel: "Save on Semble", 205 206 recordProducing: true, 206 207 needsFullScope: true, 207 208 validate,
+1
lib/actions/webhook.ts
··· 106 106 export const webhookDefinition: ActionDefinition<WebhookAction, WebhookInput, PdsWebhookAction> = { 107 107 type: "webhook", 108 108 pdsType: "run.airglow.automation#webhookAction", 109 + displayLabel: "Webhook", 109 110 recordProducing: false, 110 111 needsFullScope: false, 111 112 validate,
+65 -90
lib/automations/action-catalogue.ts
··· 1 - import { 2 - Bookmark, 3 - BookmarkPlus, 4 - FilePlus2, 5 - Heart, 6 - MessageSquare, 7 - Pencil, 8 - Trash2, 9 - UserPlus, 10 - Webhook, 11 - } from "../../app/icons.js"; 1 + import { Heart, Trash2, UserPlus } from "../../app/icons.js"; 2 + import { ACTION_REGISTRY, type ActionIcon } from "../actions/registry.js"; 12 3 import { FOLLOW_TARGETS, type ColorKey, type FollowTarget } from "./follow-targets.js"; 13 4 14 5 export type AddableActionId = ··· 20 11 | "semble-save" 21 12 | `follow-${FollowTarget}`; 22 13 14 + type Tile = { 15 + id: string; 16 + label: string; 17 + description: string; 18 + icon: ActionIcon; 19 + available: boolean; 20 + colorKey?: ColorKey; 21 + faviconDomain?: string; 22 + }; 23 + 23 24 type ActionInfo = { 24 25 label: string; 25 - icon: (typeof ACTION_CATALOGUE)[number]["actions"][number]["icon"]; 26 - catId: (typeof ACTION_CATALOGUE)[number]["id"]; 26 + icon: Tile["icon"]; 27 + catId: "webhook" | "bluesky" | "apps" | "pds"; 27 28 /** Optional override for the icon-tile color key (data-cat). Lets follow-sifa 28 29 * use a sifa-blue and follow-tangled a grey while still grouping under the 29 30 * Bluesky/Apps categories. */ 30 31 colorKey?: ColorKey; 31 - /** Domain used to render the per-app favicon next to the icon (margin-bookmark, follow). */ 32 + /** Domain used to render the per-app favicon next to the icon. */ 32 33 faviconDomain?: string; 33 34 }; 34 35 36 + /** Tile entry derived from a registered action's `catalogue` metadata. */ 37 + function tileFromRegistry(type: keyof typeof ACTION_REGISTRY): Tile { 38 + const def = ACTION_REGISTRY[type]!; 39 + const cat = def.catalogue!; 40 + return { 41 + id: type, 42 + label: cat.label, 43 + description: cat.description, 44 + icon: cat.icon, 45 + available: cat.available, 46 + ...(cat.colorKey ? { colorKey: cat.colorKey } : {}), 47 + ...(cat.faviconDomain ? { faviconDomain: cat.faviconDomain } : {}), 48 + }; 49 + } 50 + 35 51 /** Build the catalogue tile for a given follow target. Insertion order in 36 52 * `FOLLOW_TARGETS` controls the order tiles appear within their category. */ 37 - function followTileFor(target: FollowTarget) { 53 + function followTileFor(target: FollowTarget): Tile { 38 54 const t = FOLLOW_TARGETS[target]; 39 55 return { 40 - id: `follow-${target}` as const, 56 + id: `follow-${target}`, 41 57 label: t.label, 42 58 description: t.description, 43 59 icon: UserPlus, ··· 47 63 }; 48 64 } 49 65 50 - const followTilesByCat: Record<"bluesky" | "apps", ReturnType<typeof followTileFor>[]> = { 51 - bluesky: [], 52 - apps: [], 53 - }; 66 + const followTilesByCat: Record<"bluesky" | "apps", Tile[]> = { bluesky: [], apps: [] }; 54 67 for (const target of Object.keys(FOLLOW_TARGETS) as FollowTarget[]) { 55 68 followTilesByCat[FOLLOW_TARGETS[target].catId].push(followTileFor(target)); 56 69 } 57 70 71 + /** "Coming soon" placeholders that have no executor yet. Kept here so the 72 + * picker can preview the planned shape of each category. */ 73 + const COMING_SOON: Record<"bluesky" | "pds", Tile[]> = { 74 + bluesky: [ 75 + { 76 + id: "bsky-like", 77 + label: "Like a post", 78 + description: "Like a Bluesky post on your behalf", 79 + icon: Heart, 80 + available: false, 81 + }, 82 + ], 83 + pds: [ 84 + { 85 + id: "delete-record", 86 + label: "Delete a record", 87 + description: "Remove a record from a collection", 88 + icon: Trash2, 89 + available: false, 90 + }, 91 + ], 92 + }; 93 + 58 94 export const ACTION_CATALOGUE = [ 59 95 { 60 - id: "webhook", 96 + id: "webhook" as const, 61 97 label: "Webhooks", 62 98 description: "Send event data to your own server", 63 - actions: [ 64 - { 65 - id: "webhook", 66 - label: "Send a webhook", 67 - description: "POST event data to an external URL", 68 - icon: Webhook, 69 - available: true, 70 - }, 71 - ], 99 + actions: [tileFromRegistry("webhook")], 72 100 }, 73 101 { 74 - id: "bluesky", 102 + id: "bluesky" as const, 75 103 label: "Bluesky", 76 104 description: "High-level Bluesky interactions", 77 - actions: [ 78 - { 79 - id: "bsky-post", 80 - label: "Post to Bluesky", 81 - description: "Publish a post to your Bluesky account", 82 - icon: MessageSquare, 83 - available: true, 84 - }, 85 - ...followTilesByCat.bluesky, 86 - { 87 - id: "bsky-like", 88 - label: "Like a post", 89 - description: "Like a Bluesky post on your behalf", 90 - icon: Heart, 91 - available: false, 92 - }, 93 - ], 105 + actions: [tileFromRegistry("bsky-post"), ...followTilesByCat.bluesky, ...COMING_SOON.bluesky], 94 106 }, 95 107 { 96 - id: "apps", 108 + id: "apps" as const, 97 109 label: "Apps", 98 110 description: "Quick actions for specific AT Protocol apps", 99 111 actions: [ 100 - { 101 - id: "margin-bookmark", 102 - label: "Bookmark on Margin", 103 - description: "Create a bookmark note in Margin.at", 104 - icon: Bookmark, 105 - available: true, 106 - faviconDomain: "margin.at", 107 - }, 108 - { 109 - id: "semble-save", 110 - label: "Save on Semble", 111 - description: "Save a URL as a card on Semble", 112 - icon: BookmarkPlus, 113 - available: true, 114 - colorKey: "cosmik" as ColorKey, 115 - faviconDomain: "semble.so", 116 - }, 112 + tileFromRegistry("margin-bookmark"), 113 + tileFromRegistry("semble-save"), 117 114 ...followTilesByCat.apps, 118 115 ], 119 116 }, 120 117 { 121 - id: "pds", 118 + id: "pds" as const, 122 119 label: "PDS records", 123 120 description: "Low-level lexicon record operations", 124 - actions: [ 125 - { 126 - id: "record", 127 - label: "Create a record", 128 - description: "Create a new record in any collection", 129 - icon: FilePlus2, 130 - available: true, 131 - }, 132 - { 133 - id: "patch-record", 134 - label: "Update a record", 135 - description: "Modify fields of an existing record", 136 - icon: Pencil, 137 - available: true, 138 - }, 139 - { 140 - id: "delete-record", 141 - label: "Delete a record", 142 - description: "Remove a record from a collection", 143 - icon: Trash2, 144 - available: false, 145 - }, 146 - ], 121 + actions: [tileFromRegistry("record"), tileFromRegistry("patch-record"), ...COMING_SOON.pds], 147 122 }, 148 123 ]; 149 124
+7 -8
lib/automations/labels.ts
··· 16 16 "not-exists": "is missing", 17 17 }; 18 18 19 - export const actionTypeLabels: Record<string, string> = { 20 - webhook: "Webhook", 21 - record: "Create Record", 22 - "bsky-post": "Bluesky Post", 23 - "patch-record": "Update Record", 24 - "margin-bookmark": "Bookmark on Margin", 25 - follow: "Follow", 26 - }; 19 + import { ACTION_REGISTRY } from "../actions/registry.js"; 20 + 21 + /** Short noun-phrase label shown next to existing automations and in delivery 22 + * logs. Pulled from each action's registry definition. */ 23 + export const actionTypeLabels: Record<string, string> = Object.fromEntries( 24 + Object.entries(ACTION_REGISTRY).map(([t, def]) => [t, def.displayLabel]), 25 + ); 27 26 28 27 export const operationLabels: Record<string, string> = { 29 28 create: "Record created",
-13
lib/db/schema.ts
··· 89 89 | FollowAction 90 90 | SembleSaveAction; 91 91 92 - /** Action types that produce a record result (uri, cid, rkey) for chaining. */ 93 - const RECORD_PRODUCING_TYPES = new Set([ 94 - "record", 95 - "bsky-post", 96 - "patch-record", 97 - "margin-bookmark", 98 - "follow", 99 - "semble-save", 100 - ]); 101 - export function isRecordProducingAction(type: string): boolean { 102 - return RECORD_PRODUCING_TYPES.has(type); 103 - } 104 - 105 92 export type Condition = { 106 93 field: string; 107 94 operator: string;
+2 -2
lib/jetstream/handler.ts
··· 1 1 import { db } from "../db/index.js"; 2 - import { deliveryLogs, type Action, isRecordProducingAction } from "../db/schema.js"; 2 + import { deliveryLogs, type Action } from "../db/schema.js"; 3 3 import { type ActionResult } from "../actions/executor.js"; 4 - import { ACTION_REGISTRY } from "../actions/registry.js"; 4 + import { ACTION_REGISTRY, isRecordProducingAction } from "../actions/registry.js"; 5 5 import { resolveFetches } from "../actions/fetcher.js"; 6 6 import { isSuccess } from "../actions/delivery.js"; 7 7 import { type FetchContext } from "../actions/template.js";