notification manager for bsky noti.waow.tech
0

Configure Feed

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

noti / src / code-surface.ts
7.8 kB 238 lines
1import type {CallResult} from './types' 2 3export const METHOD_DEFS = [ 4 { 5 fq: 'agent.app.bsky.graph.muteActor', 6 doc: 'Mute an account by DID.', 7 inputType: 'MuteActorInput', 8 input: `interface MuteActorInput {\n actor: Did\n}`, 9 signature: `muteActor(input: MuteActorInput): Promise<void>`, 10 }, 11 { 12 fq: 'agent.app.bsky.graph.unmuteActor', 13 doc: 'Undo a previous account mute by DID.', 14 inputType: 'UnmuteActorInput', 15 input: `interface UnmuteActorInput {\n actor: Did\n}`, 16 signature: `unmuteActor(input: UnmuteActorInput): Promise<void>`, 17 }, 18 { 19 fq: 'agent.app.bsky.graph.blockActor', 20 doc: 'Block an account by DID. Blocks are public atproto records.', 21 inputType: 'BlockActorInput', 22 input: `interface BlockActorInput {\n actor: Did\n}`, 23 signature: `blockActor(input: BlockActorInput): Promise<void>`, 24 }, 25 { 26 fq: 'agent.app.bsky.graph.unblockActor', 27 doc: 'Undo a previous account block by DID.', 28 inputType: 'UnblockActorInput', 29 input: `interface UnblockActorInput {\n actor: Did\n}`, 30 signature: `unblockActor(input: UnblockActorInput): Promise<void>`, 31 }, 32 { 33 fq: 'agent.app.bsky.notification.putActivitySubscription', 34 doc: 'Change activity-subscription settings for one actor DID.', 35 inputType: 'PutActivitySubscriptionInput', 36 input: [ 37 'interface PutActivitySubscriptionInput {', 38 ' subject: Did', 39 ' activitySubscription: {', 40 ' post: boolean', 41 ' reply: boolean', 42 ' }', 43 '}', 44 ].join('\n'), 45 signature: `putActivitySubscription(input: PutActivitySubscriptionInput): Promise<void>`, 46 }, 47 { 48 fq: 'agent.app.bsky.notification.updateSeen', 49 doc: 'Advance the global notification seen cursor.', 50 inputType: 'UpdateSeenInput', 51 input: `interface UpdateSeenInput {\n seenAt: string\n}`, 52 signature: `updateSeen(input: UpdateSeenInput): Promise<void>`, 53 }, 54] as const 55 56const ALLOWED_METHODS = METHOD_DEFS.map(def => def.fq) 57 58export class CodeValidationError extends Error {} 59 60export const SDK_SURFACE = [ 61 'type Did = string', 62 '', 63 'interface EvidenceNotification {', 64 ' uri: string', 65 ' reason: string', 66 ' authorDid?: Did', 67 ' authorHandle: string', 68 ' authorName: string', 69 ' authorAvatar?: string', 70 ' indexedAt: string', 71 ' text?: string', 72 ' subjectText?: string', 73 ' isReply: boolean', 74 '}', 75 '', 76 'interface Ctx {', 77 ' sourceUris: string[]', 78 ' notifications: EvidenceNotification[]', 79 '}', 80 '', 81 ...METHOD_DEFS.flatMap(def => ['', `/** ${def.doc} */`, def.input]), 82 '', 83 'interface BlueskyAgentSurface {', 84 ' app: {', 85 ' bsky: {', 86 ' graph: {', 87 ...METHOD_DEFS.filter(def => def.fq.includes('.graph.')).map(def => ` ${def.signature}`), 88 ' }', 89 ' notification: {', 90 ...METHOD_DEFS.filter(def => def.fq.includes('.notification.')).map(def => ` ${def.signature}`), 91 ' }', 92 ' }', 93 ' }', 94 '}', 95 '', 96 'Write exactly (plain JavaScript, no type annotations):', 97 'async function run(agent, ctx) {', 98 ' // your code', 99 '}', 100].join('\n') 101 102export function validateCode(code: string) { 103 const trimmed = code.trim() 104 if (!trimmed) throw new CodeValidationError('empty code proposal') 105 if (!trimmed.includes('async function run(')) { 106 throw new CodeValidationError('proposal must define async function run(agent, ctx)') 107 } 108 109 const blocked = [ 110 'import ', 111 'require(', 112 'process.', 113 'Bun.', 114 'fetch(', 115 'XMLHttpRequest', 116 'WebSocket', 117 'setTimeout(', 118 'setInterval(', 119 ] 120 for (const token of blocked) { 121 if (trimmed.includes(token)) { 122 throw new CodeValidationError(`blocked token: ${token}`) 123 } 124 } 125 126 const matches = [...trimmed.matchAll(/agent(?:\.[A-Za-z_$][\w$]*)+/g)].map(m => m[0]) 127 for (const match of matches) { 128 const isAllowed = ALLOWED_METHODS.some(method => match.startsWith(method)) 129 if (!isAllowed) { 130 throw new CodeValidationError(`blocked agent method: ${match}`) 131 } 132 } 133} 134 135export function parseFq(fq: string) { 136 const parts = fq.replace(/^agent\./, '').split('.') 137 return {name: parts[parts.length - 1], path: parts.slice(0, -1)} 138} 139 140// biome-ignore lint/suspicious/noExplicitAny: agent is structurally typed from METHOD_DEFS 141export type AgentSurface = Record<string, any> 142 143export type QueuedCall = {method: string; input: unknown} 144 145function actorInput(input: unknown) { 146 if (!input || typeof input !== 'object' || typeof (input as {actor?: unknown}).actor !== 'string') { 147 throw new Error('expected input.actor DID') 148 } 149 return (input as {actor: string}).actor 150} 151 152async function findBlockUri(agent: AgentSurface, actor: string) { 153 let cursor: string | undefined 154 do { 155 const res = await agent.app.bsky.graph.getBlocks({cursor, limit: 100}) 156 const profile = res.data.blocks.find((row: {did?: string}) => row.did === actor) 157 const uri = profile?.viewer?.blocking 158 if (typeof uri === 'string' && uri) return uri 159 cursor = res.data.cursor 160 } while (cursor) 161 return '' 162} 163 164const METHOD_DISPATCH_OVERRIDES: Record<string, (agent: AgentSurface, input: unknown) => Promise<unknown>> = { 165 blockActor: async (agent, input) => { 166 const actor = actorInput(input) 167 return agent.app.bsky.graph.block.create( 168 {repo: agent.assertDid}, 169 {subject: actor, createdAt: new Date().toISOString()}, 170 ) 171 }, 172 unblockActor: async (agent, input) => { 173 const actor = actorInput(input) 174 const uri = await findBlockUri(agent, actor) 175 if (!uri) return undefined 176 const rkey = uri.split('/').filter(Boolean).pop() 177 if (!rkey) throw new Error(`could not parse block record URI: ${uri}`) 178 return agent.app.bsky.graph.block.delete({repo: agent.assertDid, rkey}) 179 }, 180} 181 182export const METHOD_DISPATCH: Record<string, (agent: AgentSurface, input: unknown) => Promise<unknown>> = 183 Object.fromEntries( 184 METHOD_DEFS.map(def => { 185 const {name, path} = parseFq(def.fq) 186 return [name, METHOD_DISPATCH_OVERRIDES[name] || ((agent: AgentSurface, input: unknown) => { 187 let target = agent 188 for (const segment of path) target = target[segment] 189 return target[name](input) 190 })] 191 }), 192 ) 193 194export function replayQueuedCalls( 195 agent: AgentSurface, 196 callQueue: QueuedCall[], 197): Promise<CallResult[]> { 198 return Promise.all( 199 callQueue.map(async call => { 200 const dispatch = METHOD_DISPATCH[call.method] 201 if (!dispatch) { 202 return {method: call.method, input: call.input, status: 'failed', error: 'unknown method in queue'} satisfies CallResult 203 } 204 try { 205 await dispatch(agent, call.input) 206 return {method: call.method, input: call.input, status: 'ok'} satisfies CallResult 207 } catch (err) { 208 return { 209 method: call.method, 210 input: call.input, 211 status: 'failed', 212 error: err instanceof Error ? err.message : String(err), 213 } satisfies CallResult 214 } 215 }), 216 ) 217} 218 219export const SANDBOX_PREAMBLE = (() => { 220 const grouped = new Map<string, string[]>() 221 for (const def of METHOD_DEFS) { 222 const {name, path} = parseFq(def.fq) 223 const key = path.join('.') 224 const existing = grouped.get(key) || [] 225 existing.push(` ${name}: function(input) { __calls.push({ method: "${name}", input: input }); return Promise.resolve(); }`) 226 grouped.set(key, existing) 227 } 228 const namespaces = new Map<string, string[]>() 229 for (const [ns, methods] of grouped) { 230 const parts = ns.split('.') 231 const leaf = parts[parts.length - 1] 232 namespaces.set(leaf, methods) 233 } 234 const nsBlocks = [...namespaces.entries()] 235 .map(([leaf, methods]) => ` ${leaf}: {\n${methods.join(',\n')}\n }`) 236 .join(',\n') 237 return `\nvar __calls = [];\nvar agent = {\n app: {\n bsky: {\n${nsBlocks}\n }\n }\n};\n` 238})()