+1
.idea/clippr.iml
+1
.idea/clippr.iml
···
10
10
<excludeFolder url="file://$MODULE_DIR$/backend/build" />
11
11
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
12
12
<excludeFolder url="file://$MODULE_DIR$/lexicons/dist" />
13
+
<excludeFolder url="file://$MODULE_DIR$/backend/logs" />
13
14
</content>
14
15
<orderEntry type="inheritedJdk" />
15
16
<orderEntry type="sourceFolder" forTests="false" />
+1
.idea/dictionaries/project.xml
+1
.idea/dictionaries/project.xml
+47
-1
backend/src/network/converters.ts
+47
-1
backend/src/network/converters.ts
···
14
14
UnsupportedDidMethodError,
15
15
WebDidDocumentResolver,
16
16
} from "@atcute/identity-resolver";
17
+
import { Client, simpleFetchHandler } from "@atcute/client";
17
18
18
19
/// Converts an ``At.DID`` type to a proper string, for type reasons.
19
20
export function convertDidToString(did: `did:${string}`): string {
···
30
31
}
31
32
}
32
33
33
-
// TODO: Stop leeching off Bluesky's CDN and get the blob directly from the user's PDS
34
+
// TODO: Stop leeching off the Bluesky CDN and get the blob directly from the user's PDS
35
+
// Get a CDN URI from a blob's CID
34
36
export async function getUriFromBlobCid(
35
37
did: string,
36
38
cid: string,
···
38
40
return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
39
41
}
40
42
43
+
// Get a user's handle from their DID. DID method agnostic.
41
44
export async function getHandleFromDid(did: string): Promise<string> {
42
45
const docResolver = new CompositeDidDocumentResolver({
43
46
methods: {
···
79
82
doc?.alsoKnownAs[0].lastIndexOf("/" + 1),
80
83
);
81
84
}
85
+
86
+
// Get a user's DID from their handle.
87
+
export async function getDidFromHandle(handle: string): Promise<string> {
88
+
const handler = simpleFetchHandler({
89
+
service: "https://public.api.bsky.app",
90
+
});
91
+
const rpc = new Client({ handler });
92
+
93
+
const { ok, data } = await rpc.get("com.atproto.identity.resolveHandle", {
94
+
params: {
95
+
handle: handle as `${string}.${string}`,
96
+
},
97
+
});
98
+
99
+
if (!ok) {
100
+
switch (data.error) {
101
+
case "InvalidRequest": {
102
+
throw new Error("InvalidRequest", { cause: data.message });
103
+
}
104
+
case "AccountTakedown": {
105
+
throw new Error("AccountTakedown", { cause: data.message });
106
+
}
107
+
case "AccountDeactivated": {
108
+
throw new Error("AccountDeactivated", { cause: data.message });
109
+
}
110
+
default: {
111
+
throw new Error(data.error, { cause: data.message });
112
+
}
113
+
}
114
+
}
115
+
116
+
let actorDid;
117
+
118
+
if (ok) {
119
+
actorDid = data.did as string;
120
+
}
121
+
122
+
if (actorDid === undefined) {
123
+
throw new Error("InvalidRequest");
124
+
}
125
+
126
+
return actorDid;
127
+
}
+73
-11
backend/src/routes/xrpc.ts
+73
-11
backend/src/routes/xrpc.ts
···
8
8
import { Database } from "../db/database.js";
9
9
import { usersTable } from "../db/schema.js";
10
10
import { eq } from "drizzle-orm";
11
-
import { getHandleFromDid, getUriFromBlobCid } from "../network/converters.js";
11
+
import {
12
+
getDidFromHandle,
13
+
getHandleFromDid,
14
+
getUriFromBlobCid,
15
+
} from "../network/converters.js";
12
16
13
17
const app = new Hono();
14
18
const db = Database.getInstance().getDb();
15
19
16
20
app.get("/social.clippr.actor.getProfile", async (c) => {
17
-
const did = c.req.query("did");
18
-
if (did === undefined || did.length === 0) {
21
+
const actor = c.req.query("actor");
22
+
if (actor === undefined || actor.trim().length === 0) {
19
23
return c.json(
20
24
{
21
25
error: "InvalidRequest",
22
-
message: "Error: Params must have the did property included",
26
+
message: "Error: Parameters must have the actor property included",
23
27
},
24
28
400,
25
29
);
26
30
}
27
31
32
+
let actorDid = actor;
33
+
34
+
if (!actor.startsWith("did:")) {
35
+
try {
36
+
actorDid = await getDidFromHandle(actor);
37
+
} catch (e: unknown) {
38
+
if (e instanceof Error) {
39
+
return c.json(
40
+
{
41
+
error: e.message,
42
+
message: e.cause,
43
+
},
44
+
400,
45
+
);
46
+
} else {
47
+
return c.json(
48
+
{
49
+
error: "InvalidRequest" as string,
50
+
message: "Unknown error while resolving DID from handle" as string,
51
+
},
52
+
400,
53
+
);
54
+
}
55
+
}
56
+
}
57
+
28
58
const profileSearch = await db
29
59
.selectDistinct()
30
60
.from(usersTable)
31
-
.where(eq(usersTable.did, did));
61
+
.where(eq(usersTable.did, actorDid));
32
62
33
63
if (profileSearch.length === 0) {
34
64
return c.json(
···
40
70
);
41
71
}
42
72
43
-
const handle = await getHandleFromDid(did);
73
+
let actorHandle;
74
+
75
+
if (actor.startsWith("did:")) {
76
+
try {
77
+
actorHandle = await getHandleFromDid(actor);
78
+
} catch (e: unknown) {
79
+
if (e instanceof Error) {
80
+
return c.json(
81
+
{
82
+
error: "InvalidRequest",
83
+
message: `${e.message}`,
84
+
},
85
+
400,
86
+
);
87
+
} else {
88
+
return c.json(
89
+
{
90
+
error: "InvalidRequest" as string,
91
+
message: "Unknown error while resolving handle from DID" as string,
92
+
},
93
+
400,
94
+
);
95
+
}
96
+
}
97
+
98
+
if (actorHandle === undefined) {
99
+
actorHandle = "invalid.handle";
100
+
}
101
+
} else actorHandle = actor;
102
+
44
103
// TODO: Add placeholder avatar
45
104
const avatarCid: string =
46
105
profileSearch[0]?.avatar || "https://missing.avatar";
47
-
const avatar = await getUriFromBlobCid(did, avatarCid);
106
+
let actorAvatar;
107
+
if (avatarCid !== "https://missing.avatar") {
108
+
actorAvatar = await getUriFromBlobCid(actorDid, avatarCid);
109
+
} else actorAvatar = avatarCid;
48
110
49
111
// Right now we don't do de-duplication in the database, so we just take the
50
112
// first result and use that for our return call.
51
113
return c.json({
52
-
did: did,
53
-
handle: handle,
54
-
displayName: profileSearch[0]?.displayName || null,
55
-
avatar: avatar,
114
+
did: actorDid,
115
+
handle: actorHandle,
116
+
displayName: profileSearch[0]?.displayName,
117
+
avatar: actorAvatar,
56
118
description: profileSearch[0]?.description || null,
57
119
createdAt: profileSearch[0]?.createdAt,
58
120
});
+4
-4
backend/static/api.json
+4
-4
backend/static/api.json
···
29
29
"/xrpc/social.clippr.actor.getProfile": {
30
30
"get": {
31
31
"summary": "Get a profile",
32
-
"description": "Get an user's profile based on their DID.",
32
+
"description": "Get an user's profile based on their DID or handle.",
33
33
"parameters": [
34
34
{
35
-
"name": "did",
35
+
"name": "actor",
36
36
"in": "query",
37
-
"description": "The DID of the account to get the profile record of.",
37
+
"description": "The DID or handle of the account to get the profile record of.",
38
38
"required": true,
39
39
"content": {
40
40
"schema": {
···
104
104
"message": {
105
105
"type": "string",
106
106
"description": "A detailed description of the error.",
107
-
"example": "Error: Params must have the did property included"
107
+
"example": "Error: Parameters must have the actor property included"
108
108
}
109
109
}
110
110
}