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

feat: start working on xrpc routes

+1173 -9
+70
__generated__/index.ts
··· 9 9 type StreamAuthVerifier, 10 10 } from "npm:@atproto/xrpc-server" 11 11 import { schemas } from './lexicons.ts' 12 + import * as SocialGrainGalleryGetGalleryThread from './types/social/grain/gallery/getGalleryThread.ts' 13 + import * as SocialGrainGalleryGetActorGalleries from './types/social/grain/gallery/getActorGalleries.ts' 14 + import * as SocialGrainGalleryGetGallery from './types/social/grain/gallery/getGallery.ts' 15 + import * as SocialGrainFeedGetTimeline from './types/social/grain/feed/getTimeline.ts' 16 + import * as SocialGrainActorGetProfile from './types/social/grain/actor/getProfile.ts' 12 17 13 18 export const APP_BSKY_GRAPH = { 14 19 DefsModlist: 'app.bsky.graph.defs#modlist', ··· 182 187 gallery: SocialGrainGalleryNS 183 188 graph: SocialGrainGraphNS 184 189 labeler: SocialGrainLabelerNS 190 + feed: SocialGrainFeedNS 185 191 actor: SocialGrainActorNS 186 192 photo: SocialGrainPhotoNS 187 193 ··· 190 196 this.gallery = new SocialGrainGalleryNS(server) 191 197 this.graph = new SocialGrainGraphNS(server) 192 198 this.labeler = new SocialGrainLabelerNS(server) 199 + this.feed = new SocialGrainFeedNS(server) 193 200 this.actor = new SocialGrainActorNS(server) 194 201 this.photo = new SocialGrainPhotoNS(server) 195 202 } ··· 201 208 constructor(server: Server) { 202 209 this._server = server 203 210 } 211 + 212 + getGalleryThread<AV extends AuthVerifier>( 213 + cfg: ConfigOf< 214 + AV, 215 + SocialGrainGalleryGetGalleryThread.Handler<ExtractAuth<AV>>, 216 + SocialGrainGalleryGetGalleryThread.HandlerReqCtx<ExtractAuth<AV>> 217 + >, 218 + ) { 219 + const nsid = 'social.grain.gallery.getGalleryThread' // @ts-ignore 220 + return this._server.xrpc.method(nsid, cfg) 221 + } 222 + 223 + getActorGalleries<AV extends AuthVerifier>( 224 + cfg: ConfigOf< 225 + AV, 226 + SocialGrainGalleryGetActorGalleries.Handler<ExtractAuth<AV>>, 227 + SocialGrainGalleryGetActorGalleries.HandlerReqCtx<ExtractAuth<AV>> 228 + >, 229 + ) { 230 + const nsid = 'social.grain.gallery.getActorGalleries' // @ts-ignore 231 + return this._server.xrpc.method(nsid, cfg) 232 + } 233 + 234 + getGallery<AV extends AuthVerifier>( 235 + cfg: ConfigOf< 236 + AV, 237 + SocialGrainGalleryGetGallery.Handler<ExtractAuth<AV>>, 238 + SocialGrainGalleryGetGallery.HandlerReqCtx<ExtractAuth<AV>> 239 + >, 240 + ) { 241 + const nsid = 'social.grain.gallery.getGallery' // @ts-ignore 242 + return this._server.xrpc.method(nsid, cfg) 243 + } 204 244 } 205 245 206 246 export class SocialGrainGraphNS { ··· 219 259 } 220 260 } 221 261 262 + export class SocialGrainFeedNS { 263 + _server: Server 264 + 265 + constructor(server: Server) { 266 + this._server = server 267 + } 268 + 269 + getTimeline<AV extends AuthVerifier>( 270 + cfg: ConfigOf< 271 + AV, 272 + SocialGrainFeedGetTimeline.Handler<ExtractAuth<AV>>, 273 + SocialGrainFeedGetTimeline.HandlerReqCtx<ExtractAuth<AV>> 274 + >, 275 + ) { 276 + const nsid = 'social.grain.feed.getTimeline' // @ts-ignore 277 + return this._server.xrpc.method(nsid, cfg) 278 + } 279 + } 280 + 222 281 export class SocialGrainActorNS { 223 282 _server: Server 224 283 225 284 constructor(server: Server) { 226 285 this._server = server 286 + } 287 + 288 + getProfile<AV extends AuthVerifier>( 289 + cfg: ConfigOf< 290 + AV, 291 + SocialGrainActorGetProfile.Handler<ExtractAuth<AV>>, 292 + SocialGrainActorGetProfile.HandlerReqCtx<ExtractAuth<AV>> 293 + >, 294 + ) { 295 + const nsid = 'social.grain.actor.getProfile' // @ts-ignore 296 + return this._server.xrpc.method(nsid, cfg) 227 297 } 228 298 } 229 299
+283
__generated__/lexicons.ts
··· 2718 2718 }, 2719 2719 }, 2720 2720 }, 2721 + SocialGrainGalleryGetGalleryThread: { 2722 + lexicon: 1, 2723 + id: 'social.grain.gallery.getGalleryThread', 2724 + defs: { 2725 + main: { 2726 + type: 'query', 2727 + description: 2728 + 'Gets a hydrated gallery view and its comments for a specified gallery AT-URI.', 2729 + parameters: { 2730 + type: 'params', 2731 + required: ['uri'], 2732 + properties: { 2733 + uri: { 2734 + type: 'string', 2735 + description: 2736 + 'The AT-URI of the gallery to return a hydrated view and comments for.', 2737 + format: 'at-uri', 2738 + }, 2739 + }, 2740 + }, 2741 + output: { 2742 + encoding: 'application/json', 2743 + schema: { 2744 + type: 'object', 2745 + required: ['gallery', 'comments'], 2746 + properties: { 2747 + gallery: { 2748 + type: 'ref', 2749 + ref: 'lex:social.grain.gallery.defs#galleryView', 2750 + }, 2751 + comments: { 2752 + type: 'array', 2753 + items: { 2754 + type: 'ref', 2755 + ref: 'lex:social.grain.comment.defs#commentView', 2756 + }, 2757 + }, 2758 + }, 2759 + }, 2760 + }, 2761 + }, 2762 + }, 2763 + }, 2764 + SocialGrainGalleryGetActorGalleries: { 2765 + lexicon: 1, 2766 + id: 'social.grain.gallery.getActorGalleries', 2767 + defs: { 2768 + main: { 2769 + type: 'query', 2770 + description: 2771 + "Get a view of an actor's galleries. Does not require auth.", 2772 + parameters: { 2773 + type: 'params', 2774 + required: ['actor'], 2775 + properties: { 2776 + actor: { 2777 + type: 'string', 2778 + format: 'at-identifier', 2779 + }, 2780 + limit: { 2781 + type: 'integer', 2782 + minimum: 1, 2783 + maximum: 100, 2784 + default: 50, 2785 + }, 2786 + cursor: { 2787 + type: 'string', 2788 + }, 2789 + }, 2790 + }, 2791 + output: { 2792 + encoding: 'application/json', 2793 + schema: { 2794 + type: 'object', 2795 + required: ['items'], 2796 + properties: { 2797 + cursor: { 2798 + type: 'string', 2799 + }, 2800 + items: { 2801 + type: 'array', 2802 + items: { 2803 + type: 'ref', 2804 + ref: 'lex:social.grain.gallery.defs#galleryView', 2805 + }, 2806 + }, 2807 + }, 2808 + }, 2809 + }, 2810 + errors: [ 2811 + { 2812 + name: 'BlockedActor', 2813 + }, 2814 + { 2815 + name: 'BlockedByActor', 2816 + }, 2817 + ], 2818 + }, 2819 + }, 2820 + }, 2821 + SocialGrainGalleryGetGallery: { 2822 + lexicon: 1, 2823 + id: 'social.grain.gallery.getGallery', 2824 + defs: { 2825 + main: { 2826 + type: 'query', 2827 + description: 2828 + 'Gets a hydrated gallery view for a specified gallery AT-URI.', 2829 + parameters: { 2830 + type: 'params', 2831 + required: ['uri'], 2832 + properties: { 2833 + uri: { 2834 + type: 'string', 2835 + description: 2836 + 'The AT-URI of the gallery to return a hydrated view for.', 2837 + format: 'at-uri', 2838 + }, 2839 + }, 2840 + }, 2841 + output: { 2842 + encoding: 'application/json', 2843 + schema: { 2844 + type: 'ref', 2845 + ref: 'lex:social.grain.gallery.defs#galleryView', 2846 + }, 2847 + }, 2848 + }, 2849 + }, 2850 + }, 2721 2851 SocialGrainGraphFollow: { 2722 2852 lexicon: 1, 2723 2853 id: 'social.grain.graph.follow', ··· 2942 3072 }, 2943 3073 }, 2944 3074 }, 3075 + SocialGrainFeedGetTimeline: { 3076 + lexicon: 1, 3077 + id: 'social.grain.feed.getTimeline', 3078 + defs: { 3079 + main: { 3080 + type: 'query', 3081 + description: "Get a view of the requesting account's home timeline.", 3082 + parameters: { 3083 + type: 'params', 3084 + properties: { 3085 + algorithm: { 3086 + type: 'string', 3087 + description: 3088 + "Variant 'algorithm' for timeline. Implementation-specific.", 3089 + }, 3090 + limit: { 3091 + type: 'integer', 3092 + minimum: 1, 3093 + maximum: 100, 3094 + default: 50, 3095 + }, 3096 + cursor: { 3097 + type: 'string', 3098 + }, 3099 + }, 3100 + }, 3101 + output: { 3102 + encoding: 'application/json', 3103 + schema: { 3104 + type: 'object', 3105 + required: ['feed'], 3106 + properties: { 3107 + cursor: { 3108 + type: 'string', 3109 + }, 3110 + feed: { 3111 + type: 'array', 3112 + items: { 3113 + type: 'ref', 3114 + ref: 'lex:social.grain.gallery.defs#galleryView', 3115 + }, 3116 + }, 3117 + }, 3118 + }, 3119 + }, 3120 + }, 3121 + }, 3122 + }, 2945 3123 SocialGrainFavorite: { 2946 3124 lexicon: 1, 2947 3125 id: 'social.grain.favorite', ··· 3006 3184 createdAt: { 3007 3185 type: 'string', 3008 3186 format: 'datetime', 3187 + }, 3188 + }, 3189 + }, 3190 + profileViewDetailed: { 3191 + type: 'object', 3192 + required: ['did', 'handle'], 3193 + properties: { 3194 + did: { 3195 + type: 'string', 3196 + format: 'did', 3197 + }, 3198 + handle: { 3199 + type: 'string', 3200 + format: 'handle', 3201 + }, 3202 + displayName: { 3203 + type: 'string', 3204 + maxGraphemes: 64, 3205 + maxLength: 640, 3206 + }, 3207 + description: { 3208 + type: 'string', 3209 + maxGraphemes: 256, 3210 + maxLength: 2560, 3211 + }, 3212 + avatar: { 3213 + type: 'string', 3214 + format: 'uri', 3215 + }, 3216 + followersCount: { 3217 + type: 'integer', 3218 + }, 3219 + followsCount: { 3220 + type: 'integer', 3221 + }, 3222 + galleryCount: { 3223 + type: 'integer', 3224 + }, 3225 + indexedAt: { 3226 + type: 'string', 3227 + format: 'datetime', 3228 + }, 3229 + createdAt: { 3230 + type: 'string', 3231 + format: 'datetime', 3232 + }, 3233 + viewer: { 3234 + type: 'ref', 3235 + ref: 'lex:social.grain.actor.defs#viewerState', 3236 + }, 3237 + labels: { 3238 + type: 'array', 3239 + items: { 3240 + type: 'ref', 3241 + ref: 'lex:com.atproto.label.defs#label', 3242 + }, 3243 + }, 3244 + }, 3245 + }, 3246 + viewerState: { 3247 + type: 'object', 3248 + description: 3249 + "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.", 3250 + properties: { 3251 + following: { 3252 + type: 'string', 3253 + format: 'at-uri', 3254 + }, 3255 + followedBy: { 3256 + type: 'string', 3257 + format: 'at-uri', 3258 + }, 3259 + }, 3260 + }, 3261 + }, 3262 + }, 3263 + SocialGrainActorGetProfile: { 3264 + lexicon: 1, 3265 + id: 'social.grain.actor.getProfile', 3266 + defs: { 3267 + main: { 3268 + type: 'query', 3269 + description: 3270 + 'Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.', 3271 + parameters: { 3272 + type: 'params', 3273 + required: ['actor'], 3274 + properties: { 3275 + actor: { 3276 + type: 'string', 3277 + format: 'at-identifier', 3278 + description: 'Handle or DID of account to fetch profile of.', 3279 + }, 3280 + }, 3281 + }, 3282 + output: { 3283 + encoding: 'application/json', 3284 + schema: { 3285 + type: 'ref', 3286 + ref: 'lex:social.grain.actor.defs#profileViewDetailed', 3009 3287 }, 3010 3288 }, 3011 3289 }, ··· 3547 3825 SocialGrainGalleryItem: 'social.grain.gallery.item', 3548 3826 SocialGrainGalleryDefs: 'social.grain.gallery.defs', 3549 3827 SocialGrainGallery: 'social.grain.gallery', 3828 + SocialGrainGalleryGetGalleryThread: 'social.grain.gallery.getGalleryThread', 3829 + SocialGrainGalleryGetActorGalleries: 'social.grain.gallery.getActorGalleries', 3830 + SocialGrainGalleryGetGallery: 'social.grain.gallery.getGallery', 3550 3831 SocialGrainGraphFollow: 'social.grain.graph.follow', 3551 3832 SocialGrainLabelerDefs: 'social.grain.labeler.defs', 3552 3833 SocialGrainLabelerService: 'social.grain.labeler.service', 3834 + SocialGrainFeedGetTimeline: 'social.grain.feed.getTimeline', 3553 3835 SocialGrainFavorite: 'social.grain.favorite', 3554 3836 SocialGrainActorDefs: 'social.grain.actor.defs', 3837 + SocialGrainActorGetProfile: 'social.grain.actor.getProfile', 3555 3838 SocialGrainActorProfile: 'social.grain.actor.profile', 3556 3839 SocialGrainPhotoDefs: 'social.grain.photo.defs', 3557 3840 SocialGrainPhotoExif: 'social.grain.photo.exif',
+43
__generated__/types/social/grain/actor/defs.ts
··· 35 35 export function validateProfileView<V>(v: V) { 36 36 return validate<ProfileView & V>(v, id, hashProfileView) 37 37 } 38 + 39 + export interface ProfileViewDetailed { 40 + $type?: 'social.grain.actor.defs#profileViewDetailed' 41 + did: string 42 + handle: string 43 + displayName?: string 44 + description?: string 45 + avatar?: string 46 + followersCount?: number 47 + followsCount?: number 48 + galleryCount?: number 49 + indexedAt?: string 50 + createdAt?: string 51 + viewer?: ViewerState 52 + labels?: ComAtprotoLabelDefs.Label[] 53 + } 54 + 55 + const hashProfileViewDetailed = 'profileViewDetailed' 56 + 57 + export function isProfileViewDetailed<V>(v: V) { 58 + return is$typed(v, id, hashProfileViewDetailed) 59 + } 60 + 61 + export function validateProfileViewDetailed<V>(v: V) { 62 + return validate<ProfileViewDetailed & V>(v, id, hashProfileViewDetailed) 63 + } 64 + 65 + /** Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests. */ 66 + export interface ViewerState { 67 + $type?: 'social.grain.actor.defs#viewerState' 68 + following?: string 69 + followedBy?: string 70 + } 71 + 72 + const hashViewerState = 'viewerState' 73 + 74 + export function isViewerState<V>(v: V) { 75 + return is$typed(v, id, hashViewerState) 76 + } 77 + 78 + export function validateViewerState<V>(v: V) { 79 + return validate<ViewerState & V>(v, id, hashViewerState) 80 + }
+45
__generated__/types/social/grain/actor/getProfile.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 "./defs.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.actor.getProfile"; 13 + 14 + export interface QueryParams { 15 + /** Handle or DID of account to fetch profile of. */ 16 + actor: string; 17 + } 18 + 19 + export type InputSchema = undefined; 20 + export type OutputSchema = SocialGrainActorDefs.ProfileViewDetailed; 21 + export type HandlerInput = undefined; 22 + 23 + export interface HandlerSuccess { 24 + encoding: "application/json"; 25 + body: OutputSchema; 26 + headers?: { [key: string]: string }; 27 + } 28 + 29 + export interface HandlerError { 30 + status: number; 31 + message?: string; 32 + } 33 + 34 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 35 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 36 + auth: HA; 37 + params: QueryParams; 38 + input: HandlerInput; 39 + req: express.Request; 40 + res: express.Response; 41 + resetRouteRateLimits: () => Promise<void>; 42 + }; 43 + export type Handler<HA extends HandlerAuth = never> = ( 44 + ctx: HandlerReqCtx<HA>, 45 + ) => Promise<HandlerOutput> | HandlerOutput;
+52
__generated__/types/social/grain/feed/getTimeline.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.feed.getTimeline"; 13 + 14 + export interface QueryParams { 15 + /** Variant 'algorithm' for timeline. Implementation-specific. */ 16 + algorithm?: string; 17 + limit: number; 18 + cursor?: string; 19 + } 20 + 21 + export type InputSchema = undefined; 22 + 23 + export interface OutputSchema { 24 + cursor?: string; 25 + feed: SocialGrainGalleryDefs.GalleryView[]; 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/gallery/getActorGalleries.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 "./defs.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.gallery.getActorGalleries"; 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 + error?: "BlockedActor" | "BlockedByActor"; 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
__generated__/types/social/grain/gallery/getGallery.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 "./defs.ts"; 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate; 12 + const id = "social.grain.gallery.getGallery"; 13 + 14 + export interface QueryParams { 15 + /** The AT-URI of the gallery to return a hydrated view for. */ 16 + uri: string; 17 + } 18 + 19 + export type InputSchema = undefined; 20 + export type OutputSchema = SocialGrainGalleryDefs.GalleryView; 21 + export type HandlerInput = undefined; 22 + 23 + export interface HandlerSuccess { 24 + encoding: "application/json"; 25 + body: OutputSchema; 26 + headers?: { [key: string]: string }; 27 + } 28 + 29 + export interface HandlerError { 30 + status: number; 31 + message?: string; 32 + } 33 + 34 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 35 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 36 + auth: HA; 37 + params: QueryParams; 38 + input: HandlerInput; 39 + req: express.Request; 40 + res: express.Response; 41 + resetRouteRateLimits: () => Promise<void>; 42 + }; 43 + export type Handler<HA extends HandlerAuth = never> = ( 44 + ctx: HandlerReqCtx<HA>, 45 + ) => Promise<HandlerOutput> | HandlerOutput;
+51
__generated__/types/social/grain/gallery/getGalleryThread.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 SocialGrainCommentDefs from "../comment/defs.ts"; 9 + import type * as SocialGrainGalleryDefs from "./defs.ts"; 10 + 11 + const is$typed = _is$typed, 12 + validate = _validate; 13 + const id = "social.grain.gallery.getGalleryThread"; 14 + 15 + export interface QueryParams { 16 + /** The AT-URI of the gallery to return a hydrated view and comments for. */ 17 + uri: string; 18 + } 19 + 20 + export type InputSchema = undefined; 21 + 22 + export interface OutputSchema { 23 + gallery: SocialGrainGalleryDefs.GalleryView; 24 + comments: SocialGrainCommentDefs.CommentView[]; 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 -1
deno.json
··· 3 3 "$lexicon/": "./__generated__/", 4 4 "@atproto/api": "npm:@atproto/api@^0.15.16", 5 5 "@atproto/syntax": "npm:@atproto/syntax@^0.4.0", 6 - "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.41", 6 + "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.42", 7 7 "@std/http": "jsr:@std/http@^1.0.17", 8 8 "@std/path": "jsr:@std/path@^1.0.9", 9 9 "@tailwindcss/cli": "npm:@tailwindcss/cli@^4.1.4",
+72 -5
deno.lock
··· 2 2 "version": "5", 3 3 "specifiers": { 4 4 "jsr:@bigmoves/atproto-oauth-client@0.2": "0.2.0", 5 - "jsr:@bigmoves/bff@0.3.0-beta.41": "0.3.0-beta.41", 5 + "jsr:@bigmoves/bff@0.3.0-beta.42": "0.3.0-beta.42", 6 6 "jsr:@deno/gfm@0.10": "0.10.0", 7 7 "jsr:@denosaurs/emoji@0.3": "0.3.1", 8 8 "jsr:@luca/esbuild-deno-loader@~0.11.1": "0.11.1", ··· 47 47 "npm:@atproto/oauth-client@~0.3.13": "0.3.22", 48 48 "npm:@atproto/oauth-types@~0.2.4": "0.2.8", 49 49 "npm:@atproto/syntax@0.4": "0.4.0", 50 - "npm:@atproto/xrpc-server@*": "0.7.19", 50 + "npm:@atproto/xrpc-server@*": "0.7.18", 51 51 "npm:@atproto/xrpc-server@0.7.18": "0.7.18", 52 52 "npm:@atproto/xrpc-server@0.7.19": "0.7.19", 53 53 "npm:@tailwindcss/cli@*": "4.1.9", ··· 59 59 "npm:date-fns@^4.1.0": "4.1.0", 60 60 "npm:esbuild@~0.25.5": "0.25.5", 61 61 "npm:exifr@^7.1.3": "7.1.3", 62 + "npm:express@*": "4.21.2", 62 63 "npm:github-slugger@2": "2.0.0", 63 64 "npm:he@^1.2.0": "1.2.0", 64 65 "npm:htmx.org@^1.9.12": "1.9.12", 65 66 "npm:hyperscript.org@~0.9.14": "0.9.14", 66 67 "npm:jose@5.9.6": "5.9.6", 68 + "npm:jsonwebtoken@^9.0.2": "9.0.2", 67 69 "npm:katex@0.16": "0.16.22", 68 70 "npm:marked-alert@2": "2.1.2_marked@12.0.2", 69 71 "npm:marked-footnote@^1.2.0": "1.2.4_marked@12.0.2", ··· 95 97 "npm:jose" 96 98 ] 97 99 }, 98 - "@bigmoves/bff@0.3.0-beta.41": { 99 - "integrity": "141414a26dcb44d6a08a8a259011e0a7ef01b104ff5664dafc380a6f0f9a22c0", 100 + "@bigmoves/bff@0.3.0-beta.42": { 101 + "integrity": "4866ae7f9e1a61e0abe8255f40e6ccba6d370a0ea4710cf061ebfcb5248ac688", 100 102 "dependencies": [ 101 103 "jsr:@bigmoves/atproto-oauth-client", 102 104 "jsr:@std/assert@^1.0.13", ··· 113 115 "npm:@atproto/syntax", 114 116 "npm:@atproto/xrpc-server@0.7.19", 115 117 "npm:clsx", 118 + "npm:jsonwebtoken", 116 119 "npm:multiformats@^13.3.2", 117 120 "npm:preact", 118 121 "npm:preact-render-to-string", ··· 995 998 "fill-range" 996 999 ] 997 1000 }, 1001 + "buffer-equal-constant-time@1.0.1": { 1002 + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 1003 + }, 998 1004 "buffer-from@1.1.2": { 999 1005 "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 1000 1006 }, ··· 1153 1159 "call-bind-apply-helpers", 1154 1160 "es-errors", 1155 1161 "gopd" 1162 + ] 1163 + }, 1164 + "ecdsa-sig-formatter@1.0.11": { 1165 + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 1166 + "dependencies": [ 1167 + "safe-buffer" 1156 1168 ] 1157 1169 }, 1158 1170 "ee-first@1.1.1": { ··· 1443 1455 "jose@5.9.6": { 1444 1456 "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" 1445 1457 }, 1458 + "jsonwebtoken@9.0.2": { 1459 + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 1460 + "dependencies": [ 1461 + "jws", 1462 + "lodash.includes", 1463 + "lodash.isboolean", 1464 + "lodash.isinteger", 1465 + "lodash.isnumber", 1466 + "lodash.isplainobject", 1467 + "lodash.isstring", 1468 + "lodash.once", 1469 + "ms@2.1.3", 1470 + "semver" 1471 + ] 1472 + }, 1473 + "jwa@1.4.2": { 1474 + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", 1475 + "dependencies": [ 1476 + "buffer-equal-constant-time", 1477 + "ecdsa-sig-formatter", 1478 + "safe-buffer" 1479 + ] 1480 + }, 1481 + "jws@3.2.2": { 1482 + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 1483 + "dependencies": [ 1484 + "jwa", 1485 + "safe-buffer" 1486 + ] 1487 + }, 1446 1488 "katex@0.16.22": { 1447 1489 "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", 1448 1490 "dependencies": [ ··· 1518 1560 "lightningcss-win32-x64-msvc" 1519 1561 ] 1520 1562 }, 1563 + "lodash.includes@4.3.0": { 1564 + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" 1565 + }, 1566 + "lodash.isboolean@3.0.3": { 1567 + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" 1568 + }, 1569 + "lodash.isinteger@4.0.4": { 1570 + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" 1571 + }, 1572 + "lodash.isnumber@3.0.3": { 1573 + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" 1574 + }, 1575 + "lodash.isplainobject@4.0.6": { 1576 + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" 1577 + }, 1578 + "lodash.isstring@4.0.1": { 1579 + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" 1580 + }, 1581 + "lodash.once@4.1.1": { 1582 + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" 1583 + }, 1521 1584 "lru-cache@10.4.3": { 1522 1585 "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" 1523 1586 }, ··· 1799 1862 "postcss" 1800 1863 ] 1801 1864 }, 1865 + "semver@7.7.2": { 1866 + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 1867 + "bin": true 1868 + }, 1802 1869 "send@0.19.0": { 1803 1870 "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1804 1871 "dependencies": [ ··· 2033 2100 }, 2034 2101 "workspace": { 2035 2102 "dependencies": [ 2036 - "jsr:@bigmoves/bff@0.3.0-beta.41", 2103 + "jsr:@bigmoves/bff@0.3.0-beta.42", 2037 2104 "jsr:@std/http@^1.0.17", 2038 2105 "jsr:@std/path@^1.0.9", 2039 2106 "npm:@atproto/api@~0.15.16",
+37
lexicons/social/grain/actor/defs.json
··· 28 28 "avatar": { "type": "string", "format": "uri" }, 29 29 "createdAt": { "type": "string", "format": "datetime" } 30 30 } 31 + }, 32 + "profileViewDetailed": { 33 + "type": "object", 34 + "required": ["did", "handle"], 35 + "properties": { 36 + "did": { "type": "string", "format": "did" }, 37 + "handle": { "type": "string", "format": "handle" }, 38 + "displayName": { 39 + "type": "string", 40 + "maxGraphemes": 64, 41 + "maxLength": 640 42 + }, 43 + "description": { 44 + "type": "string", 45 + "maxGraphemes": 256, 46 + "maxLength": 2560 47 + }, 48 + "avatar": { "type": "string", "format": "uri" }, 49 + "followersCount": { "type": "integer" }, 50 + "followsCount": { "type": "integer" }, 51 + "galleryCount": { "type": "integer" }, 52 + "indexedAt": { "type": "string", "format": "datetime" }, 53 + "createdAt": { "type": "string", "format": "datetime" }, 54 + "viewer": { "type": "ref", "ref": "#viewerState" }, 55 + "labels": { 56 + "type": "array", 57 + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 58 + } 59 + } 60 + }, 61 + "viewerState": { 62 + "type": "object", 63 + "description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests.", 64 + "properties": { 65 + "following": { "type": "string", "format": "at-uri" }, 66 + "followedBy": { "type": "string", "format": "at-uri" } 67 + } 31 68 } 32 69 } 33 70 }
+28
lexicons/social/grain/actor/getProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.actor.getProfile", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["actor"], 11 + "properties": { 12 + "actor": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "Handle or DID of account to fetch profile of." 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "ref", 23 + "ref": "social.grain.actor.defs#profileViewDetailed" 24 + } 25 + } 26 + } 27 + } 28 + }
+43
lexicons/social/grain/feed/getTimeline.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.feed.getTimeline", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a view of the requesting account's home timeline.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "algorithm": { 12 + "type": "string", 13 + "description": "Variant 'algorithm' for timeline. Implementation-specific." 14 + }, 15 + "limit": { 16 + "type": "integer", 17 + "minimum": 1, 18 + "maximum": 100, 19 + "default": 50 20 + }, 21 + "cursor": { "type": "string" } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "object", 28 + "required": ["feed"], 29 + "properties": { 30 + "cursor": { "type": "string" }, 31 + "feed": { 32 + "type": "array", 33 + "items": { 34 + "type": "ref", 35 + "ref": "social.grain.gallery.defs#galleryView" 36 + } 37 + } 38 + } 39 + } 40 + } 41 + } 42 + } 43 + }
+42
lexicons/social/grain/gallery/getActorGalleries.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.getActorGalleries", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a view of an actor's 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 + "errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }] 40 + } 41 + } 42 + }
+28
lexicons/social/grain/gallery/getGallery.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.getGallery", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Gets a hydrated gallery view for a specified gallery AT-URI.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["uri"], 11 + "properties": { 12 + "uri": { 13 + "type": "string", 14 + "description": "The AT-URI of the gallery to return a hydrated view for.", 15 + "format": "at-uri" 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "ref", 23 + "ref": "social.grain.gallery.defs#galleryView" 24 + } 25 + } 26 + } 27 + } 28 + }
+41
lexicons/social/grain/gallery/getGalleryThread.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.gallery.getGalleryThread", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Gets a hydrated gallery view and its comments for a specified gallery AT-URI.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["uri"], 11 + "properties": { 12 + "uri": { 13 + "type": "string", 14 + "description": "The AT-URI of the gallery to return a hydrated view and comments for.", 15 + "format": "at-uri" 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": ["gallery", "comments"], 24 + "properties": { 25 + "gallery": { 26 + "type": "ref", 27 + "ref": "social.grain.gallery.defs#galleryView" 28 + }, 29 + "comments": { 30 + "type": "array", 31 + "items": { 32 + "type": "ref", 33 + "ref": "social.grain.comment.defs#commentView" 34 + } 35 + } 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }
+142
src/api/mod.ts
··· 1 + import { 2 + OutputSchema as GetProfileOutputSchema, 3 + QueryParams as GetProfileQueryParams, 4 + } from "$lexicon/types/social/grain/actor/getProfile.ts"; 5 + import { 6 + OutputSchema as GetTimelineOutputSchema, 7 + } from "$lexicon/types/social/grain/feed/getTimeline.ts"; 8 + import { 9 + OutputSchema as GetActorGalleriesOutputSchema, 10 + QueryParams as GetActorGalleriesQueryParams, 11 + } from "$lexicon/types/social/grain/gallery/getActorGalleries.ts"; 12 + import { 13 + OutputSchema as GetGalleryOutputSchema, 14 + QueryParams as GetGalleryQueryParams, 15 + } from "$lexicon/types/social/grain/gallery/getGallery.ts"; 16 + import { 17 + OutputSchema as GetGalleryThreadOutputSchema, 18 + QueryParams as GetGalleryThreadQueryParams, 19 + } from "$lexicon/types/social/grain/gallery/getGalleryThread.ts"; 20 + import { AtUri } from "@atproto/syntax"; 21 + import { BffMiddleware, OAUTH_ROUTES, route } from "@bigmoves/bff"; 22 + import { getActorGalleries, getActorProfileDetailed } from "../lib/actor.ts"; 23 + import { BadRequestError } from "../lib/errors.ts"; 24 + import { getGallery } from "../lib/gallery.ts"; 25 + import { getTimeline } from "../lib/timeline.ts"; 26 + import { getGalleryComments } from "../modules/comments.tsx"; 27 + 28 + export const middlewares: BffMiddleware[] = [ 29 + async (req, ctx) => { 30 + const url = new URL(req.url); 31 + const { pathname } = url; 32 + 33 + if (pathname === OAUTH_ROUTES.tokenCallback) { 34 + const token = url.searchParams.get("token") ?? undefined; 35 + if (!token) { 36 + throw new BadRequestError("Missing token parameter"); 37 + } 38 + return ctx.redirect(`grainflutter://auth/callback?token=${token}`); 39 + } 40 + 41 + return ctx.next(); 42 + }, 43 + route("/oauth/session", (_req, _params, ctx) => { 44 + if (!ctx.currentUser) { 45 + return ctx.json("Unauthorized", 401); 46 + } 47 + const did = ctx.currentUser.did; 48 + const profile = getActorProfileDetailed(did, ctx); 49 + if (!profile) { 50 + return ctx.json("Profile not found", 404); 51 + } 52 + return ctx.json(profile); 53 + }), 54 + route("/xrpc/social.grain.actor.getProfile", (req, _params, ctx) => { 55 + const url = new URL(req.url); 56 + const { actor } = getProfileQueryParams(url); 57 + const profile = getActorProfileDetailed(actor, ctx); 58 + return ctx.json(profile as GetProfileOutputSchema); 59 + }), 60 + route("/xrpc/social.grain.gallery.getActorGalleries", (req, _params, ctx) => { 61 + const url = new URL(req.url); 62 + const { actor } = getActorGalleriesQueryParams(url); 63 + const galleries = getActorGalleries(actor, ctx); 64 + return ctx.json({ items: galleries } as GetActorGalleriesOutputSchema); 65 + }), 66 + route("/xrpc/social.grain.gallery.getGallery", (req, _params, ctx) => { 67 + const url = new URL(req.url); 68 + const { uri } = getGalleryQueryParams(url); 69 + const atUri = new AtUri(uri); 70 + const did = atUri.hostname; 71 + const rkey = atUri.rkey; 72 + const gallery = getGallery(did, rkey, ctx); 73 + if (!gallery) { 74 + return ctx.json("Gallery not found", 404); 75 + } 76 + return ctx.json(gallery as GetGalleryOutputSchema); 77 + }), 78 + route("/xrpc/social.grain.gallery.getGalleryThread", (req, _params, ctx) => { 79 + const url = new URL(req.url); 80 + const { uri } = getGalleryThreadQueryParams(url); 81 + const atUri = new AtUri(uri); 82 + const did = atUri.hostname; 83 + const rkey = atUri.rkey; 84 + const gallery = getGallery(did, rkey, ctx); 85 + if (!gallery) { 86 + return ctx.json("Gallery not found", 404); 87 + } 88 + const comments = getGalleryComments(uri, ctx); 89 + return ctx.json({ gallery, comments } as GetGalleryThreadOutputSchema); 90 + }), 91 + route("/xrpc/social.grain.feed.getTimeline", async (_req, _params, ctx) => { 92 + // const url = new URL(req.url); 93 + // const { algorithm, limit, cursor } = getTimelineQueryParams(url); 94 + const items = await getTimeline( 95 + ctx, 96 + "timeline", 97 + "grain", 98 + ); 99 + return ctx.json( 100 + { feed: items.map((i) => i.gallery) } as GetTimelineOutputSchema, 101 + ); 102 + }), 103 + ]; 104 + 105 + function getProfileQueryParams(url: URL): GetProfileQueryParams { 106 + const actor = url.searchParams.get("actor"); 107 + if (!actor) throw new BadRequestError("Missing actor parameter"); 108 + return { actor }; 109 + } 110 + 111 + function getActorGalleriesQueryParams(url: URL): GetActorGalleriesQueryParams { 112 + const actor = url.searchParams.get("actor"); 113 + if (!actor) throw new BadRequestError("Missing actor parameter"); 114 + const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 115 + if (isNaN(limit) || limit <= 0) { 116 + throw new BadRequestError("Invalid limit parameter"); 117 + } 118 + const cursor = url.searchParams.get("cursor") ?? undefined; 119 + return { actor, limit, cursor }; 120 + } 121 + 122 + function getGalleryQueryParams(url: URL): GetGalleryQueryParams { 123 + const uri = url.searchParams.get("uri"); 124 + if (!uri) throw new BadRequestError("Missing uri parameter"); 125 + return { uri }; 126 + } 127 + 128 + function getGalleryThreadQueryParams(url: URL): GetGalleryThreadQueryParams { 129 + const uri = url.searchParams.get("uri"); 130 + if (!uri) throw new BadRequestError("Missing uri parameter"); 131 + return { uri }; 132 + } 133 + 134 + // function getTimelineQueryParams(url: URL): GetTimelineQueryParams { 135 + // const algorithm = url.searchParams.get("algorithm") ?? undefined; 136 + // const limit = parseInt(url.searchParams.get("limit") ?? "50", 10); 137 + // if (isNaN(limit) || limit <= 0) { 138 + // throw new BadRequestError("Invalid limit parameter"); 139 + // } 140 + // const cursor = url.searchParams.get("cursor") ?? undefined; 141 + // return { algorithm, limit, cursor }; 142 + // }
+51 -2
src/lib/actor.ts
··· 1 1 import { Record as BskyProfile } from "$lexicon/types/app/bsky/actor/profile.ts"; 2 2 import { Label } from "$lexicon/types/com/atproto/label/defs.ts"; 3 3 import { Record as TangledProfile } from "$lexicon/types/sh/tangled/actor/profile.ts"; 4 - import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts"; 4 + import { 5 + ProfileView, 6 + ProfileViewDetailed, 7 + } from "$lexicon/types/social/grain/actor/defs.ts"; 5 8 import { Record as GrainProfile } from "$lexicon/types/social/grain/actor/profile.ts"; 6 9 import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts"; 7 10 import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; ··· 9 12 import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts"; 10 13 import { Un$Typed } from "$lexicon/util.ts"; 11 14 import { BffContext, WithBffMeta } from "@bigmoves/bff"; 12 - import { galleryToView, getGalleryItemsAndPhotos } from "./gallery.ts"; 15 + import { getFollowersCount, getFollowsCount } from "./follow.ts"; 16 + import { 17 + galleryToView, 18 + getGalleryCount, 19 + getGalleryItemsAndPhotos, 20 + } from "./gallery.ts"; 13 21 import { photoToView, photoUrl } from "./photo.ts"; 14 22 import type { SocialNetwork } from "./timeline.ts"; 15 23 ··· 22 30 return profileRecord ? profileToView(profileRecord, actor.handle) : null; 23 31 } 24 32 33 + export function getActorProfileDetailed(did: string, ctx: BffContext) { 34 + const actor = ctx.indexService.getActor(did); 35 + if (!actor) return null; 36 + const profileRecord = ctx.indexService.getRecord<WithBffMeta<GrainProfile>>( 37 + `at://${did}/social.grain.actor.profile/self`, 38 + ); 39 + const followersCount = getFollowersCount(did, ctx); 40 + const followsCount = getFollowsCount(did, ctx); 41 + const galleryCount = getGalleryCount(did, ctx); 42 + return profileRecord 43 + ? profileDetailedToView( 44 + profileRecord, 45 + actor.handle, 46 + followersCount, 47 + followsCount, 48 + galleryCount, 49 + ) 50 + : null; 51 + } 52 + 25 53 export function profileToView( 26 54 record: WithBffMeta<GrainProfile>, 27 55 handle: string, ··· 34 62 avatar: record?.avatar 35 63 ? photoUrl(record.did, record.avatar.ref.toString(), "thumbnail") 36 64 : undefined, 65 + }; 66 + } 67 + 68 + export function profileDetailedToView( 69 + record: WithBffMeta<GrainProfile>, 70 + handle: string, 71 + followersCount: number, 72 + followsCount: number, 73 + galleryCount: number, 74 + ): Un$Typed<ProfileViewDetailed> { 75 + return { 76 + did: record.did, 77 + handle, 78 + displayName: record.displayName, 79 + description: record.description, 80 + avatar: record?.avatar 81 + ? photoUrl(record.did, record.avatar.ref.toString(), "thumbnail") 82 + : undefined, 83 + followersCount, 84 + followsCount, 85 + galleryCount, 37 86 }; 38 87 } 39 88
+32
src/lib/follow.ts
··· 90 90 profile != null 91 91 ); 92 92 } 93 + 94 + export function getFollowersCount( 95 + followeeDid: string, 96 + ctx: BffContext, 97 + ): number { 98 + return ctx.indexService.countRecords( 99 + "social.grain.graph.follow", 100 + { 101 + orderBy: [{ field: "createdAt", direction: "desc" }], 102 + where: [{ 103 + field: "subject", 104 + equals: followeeDid, 105 + }], 106 + }, 107 + ); 108 + } 109 + 110 + export function getFollowsCount( 111 + followerDid: string, 112 + ctx: BffContext, 113 + ): number { 114 + return ctx.indexService.countRecords( 115 + "social.grain.graph.follow", 116 + { 117 + orderBy: [{ field: "createdAt", direction: "desc" }], 118 + where: [{ 119 + field: "did", 120 + equals: followerDid, 121 + }], 122 + }, 123 + ); 124 + }
+9
src/lib/gallery.ts
··· 429 429 }) 430 430 .filter((g): g is ReturnType<typeof galleryToView> => g !== null); 431 431 } 432 + 433 + export function getGalleryCount( 434 + userDid: string, 435 + ctx: BffContext, 436 + ): number { 437 + return ctx.indexService.countRecords("social.grain.gallery", { 438 + where: [{ field: "did", equals: userDid }], 439 + }); 440 + }
+2
src/main.tsx
··· 1 1 import { lexicons } from "$lexicon/lexicons.ts"; 2 2 import { bff, oauth, route } from "@bigmoves/bff"; 3 + import { middlewares as xrpcApi } from "./api/mod.ts"; 3 4 import { Root } from "./app.tsx"; 4 5 import { LoginPage } from "./components/LoginPage.tsx"; 5 6 import { PDS_HOST_URL } from "./env.ts"; ··· 55 56 rootElement: Root, 56 57 onError, 57 58 middlewares: [ 59 + ...xrpcApi, 58 60 appStateMiddleware, 59 61 oauth({ 60 62 onSignedIn,
+4 -1
src/modules/comments.tsx
··· 530 530 }, []); 531 531 } 532 532 533 - function getGalleryComments(uri: string, ctx: BffContext): CommentView[] { 533 + export function getGalleryComments( 534 + uri: string, 535 + ctx: BffContext, 536 + ): CommentView[] { 534 537 const { items: comments } = ctx.indexService.getRecords<WithBffMeta<Comment>>( 535 538 "social.grain.comment", 536 539 {