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: subscription conditions and record creation improvements

Hugo 66fc590f bd736561

+70 -17
+4 -4
app/islands/RecordFormBuilder.tsx
··· 292 292 return ( 293 293 <div class={s.fieldGroup}> 294 294 <FieldLabel name={name} required={required} htmlFor={id} /> 295 - <input 295 + <textarea 296 296 id={id} 297 - class={s.input} 298 - type="text" 297 + class={s.textarea} 298 + rows={node.format ? 1 : 2} 299 299 placeholder={node.format ? (formatHints[node.format] ?? node.format) : undefined} 300 300 value={value} 301 - onInput={(e: Event) => onValueChange((e.target as HTMLInputElement).value)} 301 + onInput={(e: Event) => onValueChange((e.target as HTMLTextAreaElement).value)} 302 302 /> 303 303 {node.description && <span class={s.hint}>{node.description}</span>} 304 304 {node.maxLength != null && <span class={s.hint}>Max length: {node.maxLength}</span>}
+5 -1
app/islands/SubscriptionForm.css.ts
··· 91 91 }); 92 92 93 93 export const conditionField = style({ 94 - flex: 2, 94 + flex: 1, 95 + }); 96 + 97 + export const conditionOperator = style({ 98 + flex: 1, 95 99 }); 96 100 97 101 export const conditionValue = style({
+25 -5
app/islands/SubscriptionForm.tsx
··· 11 11 12 12 type Condition = { 13 13 field: string; 14 + operator: string; 14 15 value: string; 15 16 }; 16 17 ··· 302 303 } 303 304 304 305 const addCondition = useCallback(() => { 305 - setConditions((prev) => [...prev, { field: "", value: "" }]); 306 + setConditions((prev) => [...prev, { field: "", operator: "eq", value: "" }]); 306 307 }, []); 307 308 308 309 const removeCondition = useCallback((index: number) => { 309 310 setConditions((prev) => prev.filter((_, i) => i !== index)); 310 311 }, []); 311 312 312 - const updateCondition = useCallback((index: number, key: "field" | "value", val: string) => { 313 - setConditions((prev) => prev.map((c, i) => (i === index ? { ...c, [key]: val } : c))); 314 - }, []); 313 + const updateCondition = useCallback( 314 + (index: number, key: "field" | "operator" | "value", val: string) => { 315 + setConditions((prev) => prev.map((c, i) => (i === index ? { ...c, [key]: val } : c))); 316 + }, 317 + [], 318 + ); 315 319 316 320 const addAction = useCallback((type: "webhook" | "record") => { 317 321 if (type === "webhook") { ··· 339 343 lexicon, 340 344 conditions: conditions 341 345 .filter((c) => c.field && c.value) 342 - .map((c) => ({ field: c.field, operator: "eq", value: c.value })), 346 + .map((c) => ({ field: c.field, operator: c.operator, value: c.value })), 343 347 actions: actions.map((a) => { 344 348 if (a.type === "webhook") { 345 349 return { type: "webhook" as const, callbackUrl: a.callbackUrl }; ··· 435 439 </span> 436 440 )} 437 441 </div> 442 + {cond.field && fields.find((f) => f.path === cond.field)?.type !== "boolean" && ( 443 + <div class={s.conditionOperator}> 444 + <select 445 + class={s.select} 446 + value={cond.operator} 447 + onChange={(e: Event) => 448 + updateCondition(i, "operator", (e.target as HTMLSelectElement).value) 449 + } 450 + > 451 + <option value="eq">equals</option> 452 + <option value="startsWith">starts with</option> 453 + <option value="endsWith">ends with</option> 454 + <option value="contains">contains</option> 455 + </select> 456 + </div> 457 + )} 438 458 <div class={s.conditionValue}> 439 459 {fields.find((f) => f.path === cond.field)?.type === "boolean" ? ( 440 460 <select
+7
app/routes/api/subscriptions/[rkey].ts
··· 20 20 | { type: "webhook"; callbackUrl: string } 21 21 | { type: "record"; targetCollection: string; recordTemplate: string }; 22 22 23 + const VALID_OPERATORS = new Set(["eq", "startsWith", "endsWith", "contains"]); 24 + 23 25 function findSubscription(did: string, rkey: string) { 24 26 return db.query.subscriptions.findFirst({ 25 27 where: and(eq(subscriptions.did, did), eq(subscriptions.rkey, rkey)), ··· 83 85 : sub.conditions; 84 86 if (conditions.length > 20) { 85 87 return c.json({ error: "Maximum 20 conditions allowed" }, 400); 88 + } 89 + for (const cond of conditions) { 90 + if (!VALID_OPERATORS.has(cond.operator)) { 91 + return c.json({ error: `Invalid condition operator: ${cond.operator}` }, 400); 92 + } 86 93 } 87 94 const active = body.active ?? sub.active; 88 95
+7
app/routes/api/subscriptions/index.ts
··· 14 14 | { type: "webhook"; callbackUrl: string } 15 15 | { type: "record"; targetCollection: string; recordTemplate: string }; 16 16 17 + const VALID_OPERATORS = new Set(["eq", "startsWith", "endsWith", "contains"]); 18 + 17 19 export const GET = createRoute(async (c) => { 18 20 const user = c.get("user"); 19 21 const rows = await db.query.subscriptions.findMany({ ··· 67 69 })); 68 70 if (conditions.length > 20) { 69 71 return c.json({ error: "Maximum 20 conditions allowed" }, 400); 72 + } 73 + for (const cond of conditions) { 74 + if (!VALID_OPERATORS.has(cond.operator)) { 75 + return c.json({ error: `Invalid condition operator: ${cond.operator}` }, 400); 76 + } 70 77 } 71 78 72 79 // Validate each action and build local + PDS action arrays
+15 -6
app/routes/dashboard/subscriptions/[rkey].tsx
··· 91 91 <Stack gap={3}> 92 92 <h3>Conditions</h3> 93 93 <ul class={plainList}> 94 - {sub.conditions.map((cond, i) => ( 95 - <li key={i}> 96 - <InlineCode>{cond.field}</InlineCode> {cond.operator}{" "} 97 - <InlineCode>{cond.value}</InlineCode> 98 - </li> 99 - ))} 94 + {sub.conditions.map((cond, i) => { 95 + const opLabels: Record<string, string> = { 96 + eq: "equals", 97 + startsWith: "starts with", 98 + endsWith: "ends with", 99 + contains: "contains", 100 + }; 101 + return ( 102 + <li key={i}> 103 + <InlineCode>{cond.field}</InlineCode>{" "} 104 + {opLabels[cond.operator] ?? cond.operator}{" "} 105 + <InlineCode>{cond.value}</InlineCode> 106 + </li> 107 + ); 108 + })} 100 109 </ul> 101 110 </Stack> 102 111 </Card>
+1 -1
lexicons/app/rglw/subscription.json
··· 89 89 "operator": { 90 90 "type": "string", 91 91 "description": "Comparison operator.", 92 - "knownValues": ["eq"], 92 + "knownValues": ["eq", "startsWith", "endsWith", "contains"], 93 93 "default": "eq", 94 94 "maxLength": 32 95 95 },
+6
lib/jetstream/matcher.ts
··· 49 49 switch (condition.operator) { 50 50 case "eq": 51 51 return actual === condition.value; 52 + case "startsWith": 53 + return actual.startsWith(condition.value); 54 + case "endsWith": 55 + return actual.endsWith(condition.value); 56 + case "contains": 57 + return actual.includes(condition.value); 52 58 default: 53 59 return false; 54 60 }