Barazo AppView backend barazo.forum
at main 158 lines 4.6 kB view raw
1import { Agent } from '@atproto/api' 2 3import type { Logger } from '../logger.js' 4 5import type { 6 PluginContext, 7 PluginSettings, 8 ScopedAtProto, 9 ScopedCache, 10 ScopedDatabase, 11} from './types.js' 12 13/** Adapter interface for the underlying cache (e.g. Valkey/ioredis). */ 14export interface CacheAdapter { 15 get(key: string): Promise<string | null> 16 set(key: string, value: string, ttlSeconds?: number): Promise<void> 17 del(key: string): Promise<void> 18} 19 20export interface PluginContextOptions { 21 pluginName: string 22 pluginVersion: string 23 permissions: string[] 24 settings: Record<string, unknown> 25 db: unknown 26 cache: CacheAdapter | null 27 oauthClient: unknown // NodeOAuthClient | null — typed as unknown to avoid coupling 28 logger: Logger 29 communityDid: string 30} 31 32function createPluginSettings(values: Record<string, unknown>): PluginSettings { 33 const copy = { ...values } 34 return { 35 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- matches PluginSettings interface 36 get<T = unknown>(key: string): T | undefined { 37 return copy[key] as T | undefined 38 }, 39 getAll(): Record<string, unknown> { 40 return { ...copy } 41 }, 42 } 43} 44 45function createScopedCache(cache: CacheAdapter, pluginName: string): ScopedCache { 46 const prefix = `plugin:${pluginName}:` 47 return { 48 get(key: string): Promise<string | null> { 49 return cache.get(`${prefix}${key}`) 50 }, 51 set(key: string, value: string, ttlSeconds?: number): Promise<void> { 52 return cache.set(`${prefix}${key}`, value, ttlSeconds) 53 }, 54 del(key: string): Promise<void> { 55 return cache.del(`${prefix}${key}`) 56 }, 57 } 58} 59 60function createScopedDatabase(db: unknown, _permissions: string[]): ScopedDatabase { 61 return { 62 execute(query: unknown): Promise<unknown> { 63 return (db as { execute(q: unknown): Promise<unknown> }).execute(query) 64 }, 65 query(_tableName: string): unknown { 66 throw new Error('ScopedDatabase.query() is not yet implemented') 67 }, 68 } 69} 70 71const BSKY_PUBLIC_API = 'https://public.api.bsky.app' 72 73interface OAuthClientLike { 74 restore(did: string): Promise<unknown> 75} 76 77function createScopedAtProto( 78 oauthClient: OAuthClientLike, 79 logger: Logger, 80 pluginName: string 81): ScopedAtProto { 82 return { 83 async getRecord(did: string, collection: string, rkey: string): Promise<unknown> { 84 try { 85 const agent = new Agent(new URL(BSKY_PUBLIC_API)) 86 const response = await agent.com.atproto.repo.getRecord({ 87 repo: did, 88 collection, 89 rkey, 90 }) 91 return response.data.value 92 } catch (err: unknown) { 93 logger.debug( 94 { err, plugin: pluginName, did, collection, rkey }, 95 'ScopedAtProto getRecord failed' 96 ) 97 return null 98 } 99 }, 100 101 async putRecord(did: string, collection: string, rkey: string, record: unknown): Promise<void> { 102 const session = await oauthClient.restore(did) 103 const agent = new Agent(session as ConstructorParameters<typeof Agent>[0]) 104 await agent.com.atproto.repo.putRecord({ 105 repo: did, 106 collection, 107 rkey, 108 record: { $type: collection, ...(record as Record<string, unknown>) }, 109 }) 110 }, 111 112 async deleteRecord(did: string, collection: string, rkey: string): Promise<void> { 113 const session = await oauthClient.restore(did) 114 const agent = new Agent(session as ConstructorParameters<typeof Agent>[0]) 115 await agent.com.atproto.repo.deleteRecord({ 116 repo: did, 117 collection, 118 rkey, 119 }) 120 }, 121 } 122} 123 124export function createPluginContext(options: PluginContextOptions): PluginContext { 125 const { 126 pluginName, 127 pluginVersion, 128 permissions, 129 settings, 130 db, 131 cache, 132 oauthClient, 133 logger, 134 communityDid, 135 } = options 136 137 const hasCachePermission = 138 permissions.includes('cache:read') || permissions.includes('cache:write') 139 140 const scopedCache = hasCachePermission && cache ? createScopedCache(cache, pluginName) : undefined 141 142 const hasPdsPermission = permissions.includes('pds:read') || permissions.includes('pds:write') 143 const scopedAtProto = 144 hasPdsPermission && oauthClient 145 ? createScopedAtProto(oauthClient as OAuthClientLike, logger, pluginName) 146 : undefined 147 148 return { 149 pluginName, 150 pluginVersion, 151 communityDid, 152 db: createScopedDatabase(db, permissions), 153 settings: createPluginSettings(settings), 154 logger: logger.child({ plugin: pluginName }), 155 ...(scopedCache ? { cache: scopedCache } : {}), 156 ...(scopedAtProto ? { atproto: scopedAtProto } : {}), 157 } satisfies PluginContext 158}