···2424## current status
25252626right now we're not running on bun because there are
2727-[some issues with partysocket](https://github.com/oven-sh/bun/issues/18807), which haven't
2828-been fixed yet and which the jetstream library we use depends on for the moment. once it has been fixed, we do plan
2929-to use bun over node in the future.
2727+[some issues with partysocket](https://github.com/oven-sh/bun/issues/18807), which haven't been
2828+fixed yet and which the jetstream library we use depends on for the moment. once it has been fixed,
2929+we do plan to use bun over node in the future.
30303131### checklist before it's usable
3232
+47-1
backend/src/network/converters.ts
···1414 UnsupportedDidMethodError,
1515 WebDidDocumentResolver,
1616} from "@atcute/identity-resolver";
1717+import { Client, simpleFetchHandler } from "@atcute/client";
17181819/// Converts an ``At.DID`` type to a proper string, for type reasons.
1920export function convertDidToString(did: `did:${string}`): string {
···3031 }
3132}
32333333-// TODO: Stop leeching off Bluesky's CDN and get the blob directly from the user's PDS
3434+// TODO: Stop leeching off the Bluesky CDN and get the blob directly from the user's PDS
3535+// Get a CDN URI from a blob's CID
3436export async function getUriFromBlobCid(
3537 did: string,
3638 cid: string,
···3840 return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
3941}
40424343+// Get a user's handle from their DID. DID method agnostic.
4144export async function getHandleFromDid(did: string): Promise<string> {
4245 const docResolver = new CompositeDidDocumentResolver({
4346 methods: {
···7982 doc?.alsoKnownAs[0].lastIndexOf("/" + 1),
8083 );
8184}
8585+8686+// Get a user's DID from their handle.
8787+export async function getDidFromHandle(handle: string): Promise<string> {
8888+ const handler = simpleFetchHandler({
8989+ service: "https://public.api.bsky.app",
9090+ });
9191+ const rpc = new Client({ handler });
9292+9393+ const { ok, data } = await rpc.get("com.atproto.identity.resolveHandle", {
9494+ params: {
9595+ handle: handle as `${string}.${string}`,
9696+ },
9797+ });
9898+9999+ if (!ok) {
100100+ switch (data.error) {
101101+ case "InvalidRequest": {
102102+ throw new Error("InvalidRequest", { cause: data.message });
103103+ }
104104+ case "AccountTakedown": {
105105+ throw new Error("AccountTakedown", { cause: data.message });
106106+ }
107107+ case "AccountDeactivated": {
108108+ throw new Error("AccountDeactivated", { cause: data.message });
109109+ }
110110+ default: {
111111+ throw new Error(data.error, { cause: data.message });
112112+ }
113113+ }
114114+ }
115115+116116+ let actorDid;
117117+118118+ if (ok) {
119119+ actorDid = data.did as string;
120120+ }
121121+122122+ if (actorDid === undefined) {
123123+ throw new Error("InvalidRequest");
124124+ }
125125+126126+ return actorDid;
127127+}
+73-11
backend/src/routes/xrpc.ts
···88import { Database } from "../db/database.js";
99import { usersTable } from "../db/schema.js";
1010import { eq } from "drizzle-orm";
1111-import { getHandleFromDid, getUriFromBlobCid } from "../network/converters.js";
1111+import {
1212+ getDidFromHandle,
1313+ getHandleFromDid,
1414+ getUriFromBlobCid,
1515+} from "../network/converters.js";
12161317const app = new Hono();
1418const db = Database.getInstance().getDb();
15191620app.get("/social.clippr.actor.getProfile", async (c) => {
1717- const did = c.req.query("did");
1818- if (did === undefined || did.length === 0) {
2121+ const actor = c.req.query("actor");
2222+ if (actor === undefined || actor.trim().length === 0) {
1923 return c.json(
2024 {
2125 error: "InvalidRequest",
2222- message: "Error: Params must have the did property included",
2626+ message: "Error: Parameters must have the actor property included",
2327 },
2428 400,
2529 );
2630 }
27313232+ let actorDid = actor;
3333+3434+ if (!actor.startsWith("did:")) {
3535+ try {
3636+ actorDid = await getDidFromHandle(actor);
3737+ } catch (e: unknown) {
3838+ if (e instanceof Error) {
3939+ return c.json(
4040+ {
4141+ error: e.message,
4242+ message: e.cause,
4343+ },
4444+ 400,
4545+ );
4646+ } else {
4747+ return c.json(
4848+ {
4949+ error: "InvalidRequest" as string,
5050+ message: "Unknown error while resolving DID from handle" as string,
5151+ },
5252+ 400,
5353+ );
5454+ }
5555+ }
5656+ }
5757+2858 const profileSearch = await db
2959 .selectDistinct()
3060 .from(usersTable)
3131- .where(eq(usersTable.did, did));
6161+ .where(eq(usersTable.did, actorDid));
32623363 if (profileSearch.length === 0) {
3464 return c.json(
···4070 );
4171 }
42724343- const handle = await getHandleFromDid(did);
7373+ let actorHandle;
7474+7575+ if (actor.startsWith("did:")) {
7676+ try {
7777+ actorHandle = await getHandleFromDid(actor);
7878+ } catch (e: unknown) {
7979+ if (e instanceof Error) {
8080+ return c.json(
8181+ {
8282+ error: "InvalidRequest",
8383+ message: `${e.message}`,
8484+ },
8585+ 400,
8686+ );
8787+ } else {
8888+ return c.json(
8989+ {
9090+ error: "InvalidRequest" as string,
9191+ message: "Unknown error while resolving handle from DID" as string,
9292+ },
9393+ 400,
9494+ );
9595+ }
9696+ }
9797+9898+ if (actorHandle === undefined) {
9999+ actorHandle = "invalid.handle";
100100+ }
101101+ } else actorHandle = actor;
102102+44103 // TODO: Add placeholder avatar
45104 const avatarCid: string =
46105 profileSearch[0]?.avatar || "https://missing.avatar";
4747- const avatar = await getUriFromBlobCid(did, avatarCid);
106106+ let actorAvatar;
107107+ if (avatarCid !== "https://missing.avatar") {
108108+ actorAvatar = await getUriFromBlobCid(actorDid, avatarCid);
109109+ } else actorAvatar = avatarCid;
4811049111 // Right now we don't do de-duplication in the database, so we just take the
50112 // first result and use that for our return call.
51113 return c.json({
5252- did: did,
5353- handle: handle,
5454- displayName: profileSearch[0]?.displayName || null,
5555- avatar: avatar,
114114+ did: actorDid,
115115+ handle: actorHandle,
116116+ displayName: profileSearch[0]?.displayName,
117117+ avatar: actorAvatar,
56118 description: profileSearch[0]?.description || null,
57119 createdAt: profileSearch[0]?.createdAt,
58120 });
+4-4
backend/static/api.json
···2929 "/xrpc/social.clippr.actor.getProfile": {
3030 "get": {
3131 "summary": "Get a profile",
3232- "description": "Get an user's profile based on their DID.",
3232+ "description": "Get an user's profile based on their DID or handle.",
3333 "parameters": [
3434 {
3535- "name": "did",
3535+ "name": "actor",
3636 "in": "query",
3737- "description": "The DID of the account to get the profile record of.",
3737+ "description": "The DID or handle of the account to get the profile record of.",
3838 "required": true,
3939 "content": {
4040 "schema": {
···104104 "message": {
105105 "type": "string",
106106 "description": "A detailed description of the error.",
107107- "example": "Error: Params must have the did property included"
107107+ "example": "Error: Parameters must have the actor property included"
108108 }
109109 }
110110 }