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

feat: add getActorFavs, update getTimeline to support following feed

Changed files
+192 -25
__generated__
types
social
lexicons
social
src
api
+12
__generated__/index.ts
··· 16 16 import * as SocialGrainFeedGetTimeline from './types/social/grain/feed/getTimeline.ts' 17 17 import * as SocialGrainActorGetProfile from './types/social/grain/actor/getProfile.ts' 18 18 import * as SocialGrainActorSearchActors from './types/social/grain/actor/searchActors.ts' 19 + import * as SocialGrainActorGetActorFavs from './types/social/grain/actor/getActorFavs.ts' 19 20 20 21 export const APP_BSKY_GRAPH = { 21 22 DefsModlist: 'app.bsky.graph.defs#modlist', ··· 327 328 >, 328 329 ) { 329 330 const nsid = 'social.grain.actor.searchActors' // @ts-ignore 331 + return this._server.xrpc.method(nsid, cfg) 332 + } 333 + 334 + getActorFavs<AV extends AuthVerifier>( 335 + cfg: ConfigOf< 336 + AV, 337 + SocialGrainActorGetActorFavs.Handler<ExtractAuth<AV>>, 338 + SocialGrainActorGetActorFavs.HandlerReqCtx<ExtractAuth<AV>> 339 + >, 340 + ) { 341 + const nsid = 'social.grain.actor.getActorFavs' // @ts-ignore 330 342 return this._server.xrpc.method(nsid, cfg) 331 343 } 332 344 }
+50 -8
__generated__/lexicons.ts
··· 2855 2855 }, 2856 2856 }, 2857 2857 }, 2858 - errors: [ 2859 - { 2860 - name: 'BlockedActor', 2861 - }, 2862 - { 2863 - name: 'BlockedByActor', 2864 - }, 2865 - ], 2866 2858 }, 2867 2859 }, 2868 2860 }, ··· 3386 3378 }, 3387 3379 }, 3388 3380 }, 3381 + SocialGrainActorGetActorFavs: { 3382 + lexicon: 1, 3383 + id: 'social.grain.actor.getActorFavs', 3384 + defs: { 3385 + main: { 3386 + type: 'query', 3387 + description: 3388 + "Get a view of an actor's favorite galleries. Does not require auth.", 3389 + parameters: { 3390 + type: 'params', 3391 + required: ['actor'], 3392 + properties: { 3393 + actor: { 3394 + type: 'string', 3395 + format: 'at-identifier', 3396 + }, 3397 + limit: { 3398 + type: 'integer', 3399 + minimum: 1, 3400 + maximum: 100, 3401 + default: 50, 3402 + }, 3403 + cursor: { 3404 + type: 'string', 3405 + }, 3406 + }, 3407 + }, 3408 + output: { 3409 + encoding: 'application/json', 3410 + schema: { 3411 + type: 'object', 3412 + required: ['items'], 3413 + properties: { 3414 + cursor: { 3415 + type: 'string', 3416 + }, 3417 + items: { 3418 + type: 'array', 3419 + items: { 3420 + type: 'ref', 3421 + ref: 'lex:social.grain.gallery.defs#galleryView', 3422 + }, 3423 + }, 3424 + }, 3425 + }, 3426 + }, 3427 + }, 3428 + }, 3429 + }, 3389 3430 SocialGrainActorProfile: { 3390 3431 lexicon: 1, 3391 3432 id: 'social.grain.actor.profile', ··· 3935 3976 SocialGrainActorDefs: 'social.grain.actor.defs', 3936 3977 SocialGrainActorGetProfile: 'social.grain.actor.getProfile', 3937 3978 SocialGrainActorSearchActors: 'social.grain.actor.searchActors', 3979 + SocialGrainActorGetActorFavs: 'social.grain.actor.getActorFavs', 3938 3980 SocialGrainActorProfile: 'social.grain.actor.profile', 3939 3981 SocialGrainPhotoDefs: 'social.grain.photo.defs', 3940 3982 SocialGrainPhotoExif: 'social.grain.photo.exif',
+51
__generated__/types/social/grain/actor/getActorFavs.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 SocialGrainGalleryDefs from "../gallery/defs.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.actor.getActorFavs"; 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: SocialGrainGalleryDefs.GalleryView[]; 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;
-1
__generated__/types/social/grain/gallery/getActorGalleries.ts
··· 35 35 export interface HandlerError { 36 36 status: number; 37 37 message?: string; 38 - error?: "BlockedActor" | "BlockedByActor"; 39 38 } 40 39 41 40 export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
+41
lexicons/social/grain/actor/getActorFavs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.actor.getActorFavs", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a view of an actor's favorite galleries. 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.gallery.defs#galleryView" 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }
+1 -2
lexicons/social/grain/gallery/getActorGalleries.json
··· 35 35 } 36 36 } 37 37 } 38 - }, 39 - "errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }] 38 + } 40 39 } 41 40 } 42 41 }
+37 -14
src/api/mod.ts
··· 1 1 import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts"; 2 2 import { 3 + OutputSchema as GetActorFavsOutputSchema, 4 + QueryParams as GetActorFavsQueryParams, 5 + } from "$lexicon/types/social/grain/actor/getActorFavs.ts"; 6 + import { 3 7 OutputSchema as GetProfileOutputSchema, 4 8 QueryParams as GetProfileQueryParams, 5 9 } from "$lexicon/types/social/grain/actor/getProfile.ts"; ··· 9 13 } from "$lexicon/types/social/grain/actor/searchActors.ts"; 10 14 import { 11 15 OutputSchema as GetTimelineOutputSchema, 16 + QueryParams as GetTimelineQueryParams, 12 17 } from "$lexicon/types/social/grain/feed/getTimeline.ts"; 13 18 import { 14 19 OutputSchema as GetActorGalleriesOutputSchema, ··· 29 34 import { BffMiddleware, route } from "@bigmoves/bff"; 30 35 import { 31 36 getActorGalleries, 37 + getActorGalleryFavs, 32 38 getActorProfileDetailed, 33 39 searchActors, 34 40 } from "../lib/actor.ts"; ··· 39 45 import { getGalleryComments } from "../modules/comments.tsx"; 40 46 41 47 export const middlewares: BffMiddleware[] = [ 42 - route("/oauth/session", (_req, _params, ctx) => { 48 + route("/oauth/session", async (_req, _params, ctx) => { 43 49 if (!ctx.currentUser) { 44 50 return ctx.json("Unauthorized", 401); 45 51 } ··· 62 68 const galleries = getActorGalleries(actor, ctx); 63 69 return ctx.json({ items: galleries } as GetActorGalleriesOutputSchema); 64 70 }), 71 + route("/xrpc/social.grain.actor.getActorFavs", (req, _params, ctx) => { 72 + const url = new URL(req.url); 73 + const { actor } = getActorFavsQueryParams(url); 74 + const galleries = getActorGalleryFavs(actor, ctx); 75 + return ctx.json({ items: galleries } as GetActorFavsOutputSchema); 76 + }), 65 77 route("/xrpc/social.grain.gallery.getGallery", (req, _params, ctx) => { 66 78 const url = new URL(req.url); 67 79 const { uri } = getGalleryQueryParams(url); ··· 87 99 const comments = getGalleryComments(uri, ctx); 88 100 return ctx.json({ gallery, comments } as GetGalleryThreadOutputSchema); 89 101 }), 90 - route("/xrpc/social.grain.feed.getTimeline", async (_req, _params, ctx) => { 91 - // const url = new URL(req.url); 92 - // const { algorithm, limit, cursor } = getTimelineQueryParams(url); 102 + route("/xrpc/social.grain.feed.getTimeline", async (req, _params, ctx) => { 103 + const url = new URL(req.url); 104 + const { algorithm } = getTimelineQueryParams(url); 93 105 const items = await getTimeline( 94 106 ctx, 95 - "timeline", 107 + algorithm === "following" ? "following" : "timeline", 96 108 "grain", 97 109 ); 98 110 return ctx.json( ··· 150 162 return { actor, limit, cursor }; 151 163 } 152 164 165 + function getActorFavsQueryParams(url: URL): GetActorFavsQueryParams { 166 + const actor = url.searchParams.get("actor"); 167 + if (!actor) throw new BadRequestError("Missing actor parameter"); 168 + const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 169 + if (isNaN(limit) || limit <= 0) { 170 + throw new BadRequestError("Invalid limit parameter"); 171 + } 172 + const cursor = url.searchParams.get("cursor") ?? undefined; 173 + return { actor, limit, cursor }; 174 + } 175 + 153 176 function getGalleryQueryParams(url: URL): GetGalleryQueryParams { 154 177 const uri = url.searchParams.get("uri"); 155 178 if (!uri) throw new BadRequestError("Missing uri parameter"); ··· 182 205 // return { limit, cursor }; 183 206 // } 184 207 185 - // function getTimelineQueryParams(url: URL): GetTimelineQueryParams { 186 - // const algorithm = url.searchParams.get("algorithm") ?? undefined; 187 - // const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 188 - // if (isNaN(limit) || limit <= 0) { 189 - // throw new BadRequestError("Invalid limit parameter"); 190 - // } 191 - // const cursor = url.searchParams.get("cursor") ?? undefined; 192 - // return { algorithm, limit, cursor }; 193 - // } 208 + function getTimelineQueryParams(url: URL): GetTimelineQueryParams { 209 + const algorithm = url.searchParams.get("algorithm") ?? undefined; 210 + const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 211 + if (isNaN(limit) || limit <= 0) { 212 + throw new BadRequestError("Invalid limit parameter"); 213 + } 214 + const cursor = url.searchParams.get("cursor") ?? undefined; 215 + return { algorithm, limit, cursor }; 216 + }