···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
···1import { Record as BskyProfile } from "$lexicon/types/app/bsky/actor/profile.ts";
2import { Label } from "$lexicon/types/com/atproto/label/defs.ts";
3import { Record as TangledProfile } from "$lexicon/types/sh/tangled/actor/profile.ts";
4-import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
0005import { Record as GrainProfile } from "$lexicon/types/social/grain/actor/profile.ts";
6import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts";
7import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
···9import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts";
10import { Un$Typed } from "$lexicon/util.ts";
11import { BffContext, WithBffMeta } from "@bigmoves/bff";
12-import { galleryToView, getGalleryItemsAndPhotos } from "./gallery.ts";
0000013import { photoToView, photoUrl } from "./photo.ts";
14import type { SocialNetwork } from "./timeline.ts";
15···22 return profileRecord ? profileToView(profileRecord, actor.handle) : null;
23}
240000000000000000000025export function profileToView(
26 record: WithBffMeta<GrainProfile>,
27 handle: string,
···34 avatar: record?.avatar
35 ? photoUrl(record.did, record.avatar.ref.toString(), "thumbnail")
36 : undefined,
00000000000000000000037 };
38}
39
···1import { Record as BskyProfile } from "$lexicon/types/app/bsky/actor/profile.ts";
2import { Label } from "$lexicon/types/com/atproto/label/defs.ts";
3import { Record as TangledProfile } from "$lexicon/types/sh/tangled/actor/profile.ts";
4+import {
5+ ProfileView,
6+ ProfileViewDetailed,
7+} from "$lexicon/types/social/grain/actor/defs.ts";
8import { Record as GrainProfile } from "$lexicon/types/social/grain/actor/profile.ts";
9import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts";
10import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
···12import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts";
13import { Un$Typed } from "$lexicon/util.ts";
14import { BffContext, WithBffMeta } from "@bigmoves/bff";
15+import { getFollowersCount, getFollowsCount } from "./follow.ts";
16+import {
17+ galleryToView,
18+ getGalleryCount,
19+ getGalleryItemsAndPhotos,
20+} from "./gallery.ts";
21import { photoToView, photoUrl } from "./photo.ts";
22import type { SocialNetwork } from "./timeline.ts";
23···30 return profileRecord ? profileToView(profileRecord, actor.handle) : null;
31}
3233+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+53export function profileToView(
54 record: WithBffMeta<GrainProfile>,
55 handle: string,
···62 avatar: record?.avatar
63 ? photoUrl(record.did, record.avatar.ref.toString(), "thumbnail")
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,
86 };
87}
88