grain.social is a photo sharing platform built on atproto.

feat: add getActorPhotos query with parameters and output schema; implement route and handler for fetching actor photos

Changed files
+175
__generated__
types
social
grain
lexicons
social
grain
src
api
+12
__generated__/index.ts
··· 19 19 import * as SocialGrainActorGetProfile from './types/social/grain/actor/getProfile.ts' 20 20 import * as SocialGrainActorSearchActors from './types/social/grain/actor/searchActors.ts' 21 21 import * as SocialGrainActorGetActorFavs from './types/social/grain/actor/getActorFavs.ts' 22 + import * as SocialGrainPhotoGetActorPhotos from './types/social/grain/photo/getActorPhotos.ts' 22 23 23 24 export const APP_BSKY_GRAPH = { 24 25 DefsModlist: 'app.bsky.graph.defs#modlist', ··· 372 373 373 374 constructor(server: Server) { 374 375 this._server = server 376 + } 377 + 378 + getActorPhotos<AV extends AuthVerifier>( 379 + cfg: ConfigOf< 380 + AV, 381 + SocialGrainPhotoGetActorPhotos.Handler<ExtractAuth<AV>>, 382 + SocialGrainPhotoGetActorPhotos.HandlerReqCtx<ExtractAuth<AV>> 383 + >, 384 + ) { 385 + const nsid = 'social.grain.photo.getActorPhotos' // @ts-ignore 386 + return this._server.xrpc.method(nsid, cfg) 375 387 } 376 388 } 377 389
+49
__generated__/lexicons.ts
··· 3859 3859 }, 3860 3860 }, 3861 3861 }, 3862 + SocialGrainPhotoGetActorPhotos: { 3863 + lexicon: 1, 3864 + id: 'social.grain.photo.getActorPhotos', 3865 + defs: { 3866 + main: { 3867 + type: 'query', 3868 + description: "Get a view of an actor's photos. Does not require auth.", 3869 + parameters: { 3870 + type: 'params', 3871 + required: ['actor'], 3872 + properties: { 3873 + actor: { 3874 + type: 'string', 3875 + format: 'at-identifier', 3876 + }, 3877 + limit: { 3878 + type: 'integer', 3879 + minimum: 1, 3880 + maximum: 100, 3881 + default: 50, 3882 + }, 3883 + cursor: { 3884 + type: 'string', 3885 + }, 3886 + }, 3887 + }, 3888 + output: { 3889 + encoding: 'application/json', 3890 + schema: { 3891 + type: 'object', 3892 + required: ['items'], 3893 + properties: { 3894 + cursor: { 3895 + type: 'string', 3896 + }, 3897 + items: { 3898 + type: 'array', 3899 + items: { 3900 + type: 'ref', 3901 + ref: 'lex:social.grain.photo.defs#photoView', 3902 + }, 3903 + }, 3904 + }, 3905 + }, 3906 + }, 3907 + }, 3908 + }, 3909 + }, 3862 3910 SocialGrainPhoto: { 3863 3911 lexicon: 1, 3864 3912 id: 'social.grain.photo', ··· 4221 4269 SocialGrainActorProfile: 'social.grain.actor.profile', 4222 4270 SocialGrainPhotoDefs: 'social.grain.photo.defs', 4223 4271 SocialGrainPhotoExif: 'social.grain.photo.exif', 4272 + SocialGrainPhotoGetActorPhotos: 'social.grain.photo.getActorPhotos', 4224 4273 SocialGrainPhoto: 'social.grain.photo', 4225 4274 ComAtprotoLabelDefs: 'com.atproto.label.defs', 4226 4275 ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
+51
__generated__/types/social/grain/photo/getActorPhotos.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server"; 5 + import express from "npm:express"; 6 + import { validate as _validate } from "../../../../lexicons.ts"; 7 + import { is$typed as _is$typed } from "../../../../util.ts"; 8 + import type * as SocialGrainPhotoDefs from "./defs.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.photo.getActorPhotos"; 13 + 14 + export interface QueryParams { 15 + actor: string; 16 + limit: number; 17 + cursor?: string; 18 + } 19 + 20 + export type InputSchema = undefined; 21 + 22 + export interface OutputSchema { 23 + cursor?: string; 24 + items: SocialGrainPhotoDefs.PhotoView[]; 25 + } 26 + 27 + export type HandlerInput = undefined; 28 + 29 + export interface HandlerSuccess { 30 + encoding: "application/json"; 31 + body: OutputSchema; 32 + headers?: { [key: string]: string }; 33 + } 34 + 35 + export interface HandlerError { 36 + status: number; 37 + message?: string; 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 41 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 42 + auth: HA; 43 + params: QueryParams; 44 + input: HandlerInput; 45 + req: express.Request; 46 + res: express.Response; 47 + resetRouteRateLimits: () => Promise<void>; 48 + }; 49 + export type Handler<HA extends HandlerAuth = never> = ( 50 + ctx: HandlerReqCtx<HA>, 51 + ) => Promise<HandlerOutput> | HandlerOutput;
+41
lexicons/social/grain/photo/getActorPhotos.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.photo.getActorPhotos", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a view of an actor's photos. Does not require auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actor"], 11 + "properties": { 12 + "actor": { "type": "string", "format": "at-identifier" }, 13 + "limit": { 14 + "type": "integer", 15 + "minimum": 1, 16 + "maximum": 100, 17 + "default": 50 18 + }, 19 + "cursor": { "type": "string" } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["items"], 27 + "properties": { 28 + "cursor": { "type": "string" }, 29 + "items": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "social.grain.photo.defs#photoView" 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }
+22
src/api/mod.ts
··· 38 38 import { 39 39 OutputSchema as GetNotificationsOutputSchema, 40 40 } from "$lexicon/types/social/grain/notification/getNotifications.ts"; 41 + import { 42 + OutputSchema as GetActorPhotosOutputSchema, 43 + QueryParams as GetActorPhotosQueryParams, 44 + } from "$lexicon/types/social/grain/photo/getActorPhotos.ts"; 41 45 42 46 import { AtUri } from "@atproto/syntax"; 43 47 import { BffMiddleware, route } from "@bigmoves/bff"; 44 48 import { 45 49 getActorGalleries, 46 50 getActorGalleryFavs, 51 + getActorPhotos, 47 52 getActorProfile, 48 53 getActorProfileDetailed, 49 54 searchActors, ··· 87 92 const { actor } = getActorFavsQueryParams(url); 88 93 const galleries = getActorGalleryFavs(actor, ctx); 89 94 return ctx.json({ items: galleries } as GetActorFavsOutputSchema); 95 + }), 96 + route("/xrpc/social.grain.photo.getActorPhotos", (req, _params, ctx) => { 97 + const url = new URL(req.url); 98 + const { actor } = getActorPhotosQueryParams(url); 99 + const photos = getActorPhotos(actor, ctx); 100 + return ctx.json({ items: photos } as GetActorPhotosOutputSchema); 90 101 }), 91 102 route("/xrpc/social.grain.gallery.getGallery", (req, _params, ctx) => { 92 103 const url = new URL(req.url); ··· 208 219 } 209 220 210 221 function getActorFavsQueryParams(url: URL): GetActorFavsQueryParams { 222 + const actor = url.searchParams.get("actor"); 223 + if (!actor) throw new BadRequestError("Missing actor parameter"); 224 + const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 225 + if (isNaN(limit) || limit <= 0) { 226 + throw new BadRequestError("Invalid limit parameter"); 227 + } 228 + const cursor = url.searchParams.get("cursor") ?? undefined; 229 + return { actor, limit, cursor }; 230 + } 231 + 232 + function getActorPhotosQueryParams(url: URL): GetActorPhotosQueryParams { 211 233 const actor = url.searchParams.get("actor"); 212 234 if (!actor) throw new BadRequestError("Missing actor parameter"); 213 235 const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);