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

feat: add getFollowers and getFollows endpoints with corresponding lexicons and types; update xrpc routes to handle requests

Changed files
+381 -8
__generated__
lexicons
social
src
api
+24
__generated__/index.ts
··· 13 13 import * as SocialGrainGalleryGetGalleryThread from './types/social/grain/gallery/getGalleryThread.ts' 14 14 import * as SocialGrainGalleryGetActorGalleries from './types/social/grain/gallery/getActorGalleries.ts' 15 15 import * as SocialGrainGalleryGetGallery from './types/social/grain/gallery/getGallery.ts' 16 + import * as SocialGrainGraphGetFollowers from './types/social/grain/graph/getFollowers.ts' 17 + import * as SocialGrainGraphGetFollows from './types/social/grain/graph/getFollows.ts' 16 18 import * as SocialGrainFeedGetTimeline from './types/social/grain/feed/getTimeline.ts' 17 19 import * as SocialGrainActorGetProfile from './types/social/grain/actor/getProfile.ts' 18 20 import * as SocialGrainActorSearchActors from './types/social/grain/actor/searchActors.ts' ··· 272 274 273 275 constructor(server: Server) { 274 276 this._server = server 277 + } 278 + 279 + getFollowers<AV extends AuthVerifier>( 280 + cfg: ConfigOf< 281 + AV, 282 + SocialGrainGraphGetFollowers.Handler<ExtractAuth<AV>>, 283 + SocialGrainGraphGetFollowers.HandlerReqCtx<ExtractAuth<AV>> 284 + >, 285 + ) { 286 + const nsid = 'social.grain.graph.getFollowers' // @ts-ignore 287 + return this._server.xrpc.method(nsid, cfg) 288 + } 289 + 290 + getFollows<AV extends AuthVerifier>( 291 + cfg: ConfigOf< 292 + AV, 293 + SocialGrainGraphGetFollows.Handler<ExtractAuth<AV>>, 294 + SocialGrainGraphGetFollows.HandlerReqCtx<ExtractAuth<AV>> 295 + >, 296 + ) { 297 + const nsid = 'social.grain.graph.getFollows' // @ts-ignore 298 + return this._server.xrpc.method(nsid, cfg) 275 299 } 276 300 } 277 301
+108
__generated__/lexicons.ts
··· 2948 2948 }, 2949 2949 }, 2950 2950 }, 2951 + SocialGrainGraphGetFollowers: { 2952 + lexicon: 1, 2953 + id: 'social.grain.graph.getFollowers', 2954 + defs: { 2955 + main: { 2956 + type: 'query', 2957 + description: 2958 + 'Enumerates accounts which follow a specified account (actor).', 2959 + parameters: { 2960 + type: 'params', 2961 + required: ['actor'], 2962 + properties: { 2963 + actor: { 2964 + type: 'string', 2965 + format: 'at-identifier', 2966 + }, 2967 + limit: { 2968 + type: 'integer', 2969 + minimum: 1, 2970 + maximum: 100, 2971 + default: 50, 2972 + }, 2973 + cursor: { 2974 + type: 'string', 2975 + }, 2976 + }, 2977 + }, 2978 + output: { 2979 + encoding: 'application/json', 2980 + schema: { 2981 + type: 'object', 2982 + required: ['subject', 'followers'], 2983 + properties: { 2984 + subject: { 2985 + type: 'ref', 2986 + ref: 'lex:social.grain.actor.defs#profileView', 2987 + }, 2988 + cursor: { 2989 + type: 'string', 2990 + }, 2991 + followers: { 2992 + type: 'array', 2993 + items: { 2994 + type: 'ref', 2995 + ref: 'lex:social.grain.actor.defs#profileView', 2996 + }, 2997 + }, 2998 + }, 2999 + }, 3000 + }, 3001 + }, 3002 + }, 3003 + }, 3004 + SocialGrainGraphGetFollows: { 3005 + lexicon: 1, 3006 + id: 'social.grain.graph.getFollows', 3007 + defs: { 3008 + main: { 3009 + type: 'query', 3010 + description: 3011 + 'Enumerates accounts which a specified account (actor) follows.', 3012 + parameters: { 3013 + type: 'params', 3014 + required: ['actor'], 3015 + properties: { 3016 + actor: { 3017 + type: 'string', 3018 + format: 'at-identifier', 3019 + }, 3020 + limit: { 3021 + type: 'integer', 3022 + minimum: 1, 3023 + maximum: 100, 3024 + default: 50, 3025 + }, 3026 + cursor: { 3027 + type: 'string', 3028 + }, 3029 + }, 3030 + }, 3031 + output: { 3032 + encoding: 'application/json', 3033 + schema: { 3034 + type: 'object', 3035 + required: ['subject', 'follows'], 3036 + properties: { 3037 + subject: { 3038 + type: 'ref', 3039 + ref: 'lex:social.grain.actor.defs#profileView', 3040 + }, 3041 + cursor: { 3042 + type: 'string', 3043 + }, 3044 + follows: { 3045 + type: 'array', 3046 + items: { 3047 + type: 'ref', 3048 + ref: 'lex:social.grain.actor.defs#profileView', 3049 + }, 3050 + }, 3051 + }, 3052 + }, 3053 + }, 3054 + }, 3055 + }, 3056 + }, 2951 3057 SocialGrainLabelerDefs: { 2952 3058 lexicon: 1, 2953 3059 id: 'social.grain.labeler.defs', ··· 4021 4127 SocialGrainGalleryGetActorGalleries: 'social.grain.gallery.getActorGalleries', 4022 4128 SocialGrainGalleryGetGallery: 'social.grain.gallery.getGallery', 4023 4129 SocialGrainGraphFollow: 'social.grain.graph.follow', 4130 + SocialGrainGraphGetFollowers: 'social.grain.graph.getFollowers', 4131 + SocialGrainGraphGetFollows: 'social.grain.graph.getFollows', 4024 4132 SocialGrainLabelerDefs: 'social.grain.labeler.defs', 4025 4133 SocialGrainLabelerService: 'social.grain.labeler.service', 4026 4134 SocialGrainFeedGetTimeline: 'social.grain.feed.getTimeline',
+52
__generated__/types/social/grain/graph/getFollowers.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 SocialGrainActorDefs from "../actor/defs.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.graph.getFollowers"; 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 + subject: SocialGrainActorDefs.ProfileView; 24 + cursor?: string; 25 + followers: SocialGrainActorDefs.ProfileView[]; 26 + } 27 + 28 + export type HandlerInput = undefined; 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/graph/getFollows.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 SocialGrainActorDefs from "../actor/defs.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.graph.getFollows"; 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 + subject: SocialGrainActorDefs.ProfileView; 24 + cursor?: string; 25 + follows: SocialGrainActorDefs.ProfileView[]; 26 + } 27 + 28 + export type HandlerInput = undefined; 29 + 30 + export interface HandlerSuccess { 31 + encoding: "application/json"; 32 + body: OutputSchema; 33 + headers?: { [key: string]: string }; 34 + } 35 + 36 + export interface HandlerError { 37 + status: number; 38 + message?: string; 39 + } 40 + 41 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 42 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 43 + auth: HA; 44 + params: QueryParams; 45 + input: HandlerInput; 46 + req: express.Request; 47 + res: express.Response; 48 + resetRouteRateLimits: () => Promise<void>; 49 + }; 50 + export type Handler<HA extends HandlerAuth = never> = ( 51 + ctx: HandlerReqCtx<HA>, 52 + ) => Promise<HandlerOutput> | HandlerOutput;
+45
lexicons/social/grain/graph/getFollowers.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.graph.getFollowers", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Enumerates accounts which follow a specified account (actor).", 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": ["subject", "followers"], 27 + "properties": { 28 + "subject": { 29 + "type": "ref", 30 + "ref": "social.grain.actor.defs#profileView" 31 + }, 32 + "cursor": { "type": "string" }, 33 + "followers": { 34 + "type": "array", 35 + "items": { 36 + "type": "ref", 37 + "ref": "social.grain.actor.defs#profileView" 38 + } 39 + } 40 + } 41 + } 42 + } 43 + } 44 + } 45 + }
+45
lexicons/social/grain/graph/getFollows.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.graph.getFollows", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Enumerates accounts which a specified account (actor) follows.", 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": ["subject", "follows"], 27 + "properties": { 28 + "subject": { 29 + "type": "ref", 30 + "ref": "social.grain.actor.defs#profileView" 31 + }, 32 + "cursor": { "type": "string" }, 33 + "follows": { 34 + "type": "array", 35 + "items": { 36 + "type": "ref", 37 + "ref": "social.grain.actor.defs#profileView" 38 + } 39 + } 40 + } 41 + } 42 + } 43 + } 44 + } 45 + }
+55 -8
src/api/mod.ts
··· 28 28 QueryParams as GetGalleryThreadQueryParams, 29 29 } from "$lexicon/types/social/grain/gallery/getGalleryThread.ts"; 30 30 import { 31 + OutputSchema as GetFollowersOutputSchema, 32 + QueryParams as GetFollowersQueryParams, 33 + } from "$lexicon/types/social/grain/graph/getFollowers.ts"; 34 + import { 35 + OutputSchema as GetFollowsOutputSchema, 36 + QueryParams as GetFollowsQueryParams, 37 + } from "$lexicon/types/social/grain/graph/getFollows.ts"; 38 + import { 31 39 OutputSchema as GetNotificationsOutputSchema, 32 40 } from "$lexicon/types/social/grain/notification/getNotifications.ts"; 41 + 33 42 import { AtUri } from "@atproto/syntax"; 34 43 import { BffMiddleware, route } from "@bigmoves/bff"; 35 44 import { 36 45 getActorGalleries, 37 46 getActorGalleryFavs, 47 + getActorProfile, 38 48 getActorProfileDetailed, 39 49 searchActors, 40 50 } from "../lib/actor.ts"; 41 51 import { BadRequestError } from "../lib/errors.ts"; 52 + import { 53 + getFollowersWithProfiles, 54 + getFollowingWithProfiles, 55 + } from "../lib/follow.ts"; 42 56 import { getGalleriesByHashtag, getGallery } from "../lib/gallery.ts"; 43 57 import { getNotifications } from "../lib/notifications.ts"; 44 58 import { getTimeline } from "../lib/timeline.ts"; ··· 154 168 ); 155 169 }, 156 170 ), 171 + route("/xrpc/social.grain.graph.getFollows", (req, _params, ctx) => { 172 + const url = new URL(req.url); 173 + const { actor } = getFollowsQueryParams(url); 174 + const subject = getActorProfile(actor, ctx); 175 + const follows = getFollowingWithProfiles(actor, ctx); 176 + return ctx.json({ 177 + subject, 178 + follows, 179 + } as GetFollowsOutputSchema); 180 + }), 181 + route("/xrpc/social.grain.graph.getFollowers", (req, _params, ctx) => { 182 + const url = new URL(req.url); 183 + const { actor } = getFollowersQueryParams(url); 184 + const subject = getActorProfile(actor, ctx); 185 + const followers = getFollowersWithProfiles(actor, ctx); 186 + return ctx.json({ 187 + subject, 188 + followers, 189 + } as GetFollowersOutputSchema); 190 + }), 157 191 ]; 158 192 159 193 function getProfileQueryParams(url: URL): GetProfileQueryParams { ··· 207 241 return { q, limit, cursor }; 208 242 } 209 243 210 - // function getNotificationsQueryParams(url: URL): GetNotificationsQueryParams { 211 - // const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 212 - // if (isNaN(limit) || limit <= 0) { 213 - // throw new BadRequestError("Invalid limit parameter"); 214 - // } 215 - // const cursor = url.searchParams.get("cursor") ?? undefined; 216 - // return { limit, cursor }; 217 - // } 244 + function getFollowsQueryParams(url: URL): GetFollowsQueryParams { 245 + const actor = url.searchParams.get("actor"); 246 + if (!actor) throw new BadRequestError("Missing actor parameter"); 247 + const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 248 + if (isNaN(limit) || limit <= 0) { 249 + throw new BadRequestError("Invalid limit parameter"); 250 + } 251 + const cursor = url.searchParams.get("cursor") ?? undefined; 252 + return { actor, limit, cursor }; 253 + } 254 + 255 + function getFollowersQueryParams(url: URL): GetFollowersQueryParams { 256 + const actor = url.searchParams.get("actor"); 257 + if (!actor) throw new BadRequestError("Missing actor parameter"); 258 + const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 259 + if (isNaN(limit) || limit <= 0) { 260 + throw new BadRequestError("Invalid limit parameter"); 261 + } 262 + const cursor = url.searchParams.get("cursor") ?? undefined; 263 + return { actor, limit, cursor }; 264 + } 218 265 219 266 function getTimelineQueryParams(url: URL): GetTimelineQueryParams { 220 267 const algorithm = url.searchParams.get("algorithm") ?? undefined;