the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add actor getProfile lexicon and handler

Add lexicon documents and generated types for actor profile
(io.pocketenv.actor.defs and getProfile). Implement an xrpc handler to
resolve handles, query AT Protocol for profiles, refresh DB records,
and present ProfileViewDetailed (with retry and timeout). Add web API
client, React hook, and Profile type. Add lodash dependency.

+691 -43
+6
apps/api/bun.lock
··· 36 36 "jsonwebtoken": "^9.0.3", 37 37 "kysely": "^0.28.11", 38 38 "libsodium-wrappers": "^0.8.2", 39 + "lodash": "^4.17.23", 39 40 "morgan": "^1.10.1", 40 41 "pg": "^8.18.0", 41 42 "prompts": "^2.4.2", ··· 52 53 "@types/cors": "^2.8.19", 53 54 "@types/express": "^5.0.6", 54 55 "@types/jsonwebtoken": "^9.0.10", 56 + "@types/lodash": "^4.17.23", 55 57 "@types/morgan": "^1.9.10", 56 58 "@types/pg": "^8.16.0", 57 59 "drizzle-kit": "^0.31.9", ··· 259 261 "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], 260 262 261 263 "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], 264 + 265 + "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], 262 266 263 267 "@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="], 264 268 ··· 531 535 "libsodium": ["libsodium@0.8.2", "", {}, "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw=="], 532 536 533 537 "libsodium-wrappers": ["libsodium-wrappers@0.8.2", "", { "dependencies": { "libsodium": "^0.8.0" } }, "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw=="], 538 + 539 + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], 534 540 535 541 "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], 536 542
+42
apps/api/lexicons/actor/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.actor.defs", 4 + "defs": { 5 + "profileViewDetailed": { 6 + "type": "object", 7 + "properties": { 8 + "id": { 9 + "type": "string", 10 + "description": "The unique identifier of the actor." 11 + }, 12 + "did": { 13 + "type": "string", 14 + "description": "The DID of the actor." 15 + }, 16 + "handle": { 17 + "type": "string", 18 + "description": "The handle of the actor." 19 + }, 20 + "displayName": { 21 + "type": "string", 22 + "description": "The display name of the actor." 23 + }, 24 + "avatar": { 25 + "type": "string", 26 + "description": "The URL of the actor's avatar image.", 27 + "format": "uri" 28 + }, 29 + "createdAt": { 30 + "type": "string", 31 + "description": "The date and time when the actor was created.", 32 + "format": "datetime" 33 + }, 34 + "updatedAt": { 35 + "type": "string", 36 + "description": "The date and time when the actor was last updated.", 37 + "format": "datetime" 38 + } 39 + } 40 + } 41 + } 42 + }
+27
apps/api/lexicons/actor/getProfile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.pocketenv.actor.getProfile", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the profile of an actor", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "did": { 12 + "type": "string", 13 + "description": "The DID or handle of the actor", 14 + "format": "at-identifier" 15 + } 16 + } 17 + }, 18 + "output": { 19 + "encoding": "application/json", 20 + "schema": { 21 + "type": "ref", 22 + "ref": "io.pocketenv.actor.defs#profileViewDetailed" 23 + } 24 + } 25 + } 26 + } 27 + }
+2
apps/api/package.json
··· 45 45 "jsonwebtoken": "^9.0.3", 46 46 "kysely": "^0.28.11", 47 47 "libsodium-wrappers": "^0.8.2", 48 + "lodash": "^4.17.23", 48 49 "morgan": "^1.10.1", 49 50 "pg": "^8.18.0", 50 51 "prompts": "^2.4.2", ··· 61 62 "@types/cors": "^2.8.19", 62 63 "@types/express": "^5.0.6", 63 64 "@types/jsonwebtoken": "^9.0.10", 65 + "@types/lodash": "^4.17.23", 64 66 "@types/morgan": "^1.9.10", 65 67 "@types/pg": "^8.16.0", 66 68 "drizzle-kit": "^0.31.9",
+48
apps/api/pkl/defs/actor/defs.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.actor.defs" 5 + defs = new Mapping<String, ObjectType> { 6 + ["profileViewDetailed"] { 7 + type = "object" 8 + properties { 9 + ["id"] = new StringType { 10 + type = "string" 11 + description = "The unique identifier of the actor." 12 + } 13 + 14 + ["did"] = new StringType { 15 + type = "string" 16 + description = "The DID of the actor." 17 + } 18 + 19 + ["handle"] = new StringType { 20 + type = "string" 21 + description = "The handle of the actor." 22 + } 23 + 24 + ["displayName"] = new StringType { 25 + type = "string" 26 + description = "The display name of the actor." 27 + } 28 + 29 + ["avatar"] = new StringType { 30 + type = "string" 31 + format = "uri" 32 + description = "The URL of the actor's avatar image." 33 + } 34 + 35 + ["createdAt"] = new StringType { 36 + type = "string" 37 + format = "datetime" 38 + description = "The date and time when the actor was created." 39 + } 40 + 41 + ["updatedAt"] = new StringType { 42 + type = "string" 43 + format = "datetime" 44 + description = "The date and time when the actor was last updated." 45 + } 46 + } 47 + } 48 + }
+24
apps/api/pkl/defs/actor/getProfile.pkl
··· 1 + amends "../../schema/lexicon.pkl" 2 + 3 + lexicon = 1 4 + id = "io.pocketenv.actor.getProfile" 5 + defs = new Mapping<String, Query> { 6 + ["main"] { 7 + type = "query" 8 + description = "Get the profile of an actor" 9 + parameters = new Params { 10 + properties { 11 + ["did"] = new StringType { 12 + description = "The DID or handle of the actor" 13 + format = "at-identifier" 14 + } 15 + } 16 + } 17 + output { 18 + encoding = "application/json" 19 + schema = new Ref { 20 + ref = "io.pocketenv.actor.defs#profileViewDetailed" 21 + } 22 + } 23 + } 24 + }
+49 -27
apps/api/src/lexicon/index.ts
··· 9 9 type StreamAuthVerifier, 10 10 } from "@atproto/xrpc-server"; 11 11 import { schemas } from "./lexicons"; 12 + import type * as IoPocketenvActorGetProfile from "./types/io/pocketenv/actor/getProfile"; 12 13 import type * as IoPocketenvSandboxClaimSandbox from "./types/io/pocketenv/sandbox/claimSandbox"; 13 14 import type * as IoPocketenvSandboxCreateSandbox from "./types/io/pocketenv/sandbox/createSandbox"; 14 15 import type * as IoPocketenvSandboxDeleteSandbox from "./types/io/pocketenv/sandbox/deleteSandbox"; ··· 23 24 24 25 export class Server { 25 26 xrpc: XrpcServer; 26 - app: AppNS; 27 27 io: IoNS; 28 + app: AppNS; 28 29 com: ComNS; 29 30 30 31 constructor(options?: XrpcOptions) { 31 32 this.xrpc = createXrpcServer(schemas, options); 32 - this.app = new AppNS(this); 33 33 this.io = new IoNS(this); 34 + this.app = new AppNS(this); 34 35 this.com = new ComNS(this); 35 36 } 36 37 } 37 38 38 - export class AppNS { 39 + export class IoNS { 39 40 _server: Server; 40 - bsky: AppBskyNS; 41 - 42 - constructor(server: Server) { 43 - this._server = server; 44 - this.bsky = new AppBskyNS(server); 45 - } 46 - } 47 - 48 - export class AppBskyNS { 49 - _server: Server; 50 - actor: AppBskyActorNS; 41 + pocketenv: IoPocketenvNS; 51 42 52 43 constructor(server: Server) { 53 44 this._server = server; 54 - this.actor = new AppBskyActorNS(server); 45 + this.pocketenv = new IoPocketenvNS(server); 55 46 } 56 47 } 57 48 58 - export class AppBskyActorNS { 49 + export class IoPocketenvNS { 59 50 _server: Server; 51 + actor: IoPocketenvActorNS; 52 + sandbox: IoPocketenvSandboxNS; 60 53 61 54 constructor(server: Server) { 62 55 this._server = server; 56 + this.actor = new IoPocketenvActorNS(server); 57 + this.sandbox = new IoPocketenvSandboxNS(server); 63 58 } 64 59 } 65 60 66 - export class IoNS { 61 + export class IoPocketenvActorNS { 67 62 _server: Server; 68 - pocketenv: IoPocketenvNS; 69 63 70 64 constructor(server: Server) { 71 65 this._server = server; 72 - this.pocketenv = new IoPocketenvNS(server); 73 66 } 74 - } 75 67 76 - export class IoPocketenvNS { 77 - _server: Server; 78 - sandbox: IoPocketenvSandboxNS; 79 - 80 - constructor(server: Server) { 81 - this._server = server; 82 - this.sandbox = new IoPocketenvSandboxNS(server); 68 + getProfile<AV extends AuthVerifier>( 69 + cfg: ConfigOf< 70 + AV, 71 + IoPocketenvActorGetProfile.Handler<ExtractAuth<AV>>, 72 + IoPocketenvActorGetProfile.HandlerReqCtx<ExtractAuth<AV>> 73 + >, 74 + ) { 75 + const nsid = "io.pocketenv.actor.getProfile"; // @ts-ignore 76 + return this._server.xrpc.method(nsid, cfg); 83 77 } 84 78 } 85 79 ··· 165 159 ) { 166 160 const nsid = "io.pocketenv.sandbox.stopSandbox"; // @ts-ignore 167 161 return this._server.xrpc.method(nsid, cfg); 162 + } 163 + } 164 + 165 + export class AppNS { 166 + _server: Server; 167 + bsky: AppBskyNS; 168 + 169 + constructor(server: Server) { 170 + this._server = server; 171 + this.bsky = new AppBskyNS(server); 172 + } 173 + } 174 + 175 + export class AppBskyNS { 176 + _server: Server; 177 + actor: AppBskyActorNS; 178 + 179 + constructor(server: Server) { 180 + this._server = server; 181 + this.actor = new AppBskyActorNS(server); 182 + } 183 + } 184 + 185 + export class AppBskyActorNS { 186 + _server: Server; 187 + 188 + constructor(server: Server) { 189 + this._server = server; 168 190 } 169 191 } 170 192
+71
apps/api/src/lexicon/lexicons.ts
··· 4 4 import { type LexiconDoc, Lexicons } from "@atproto/lexicon"; 5 5 6 6 export const schemaDict = { 7 + IoPocketenvActorDefs: { 8 + lexicon: 1, 9 + id: "io.pocketenv.actor.defs", 10 + defs: { 11 + profileViewDetailed: { 12 + type: "object", 13 + properties: { 14 + id: { 15 + type: "string", 16 + description: "The unique identifier of the actor.", 17 + }, 18 + did: { 19 + type: "string", 20 + description: "The DID of the actor.", 21 + }, 22 + handle: { 23 + type: "string", 24 + description: "The handle of the actor.", 25 + }, 26 + displayName: { 27 + type: "string", 28 + description: "The display name of the actor.", 29 + }, 30 + avatar: { 31 + type: "string", 32 + description: "The URL of the actor's avatar image.", 33 + format: "uri", 34 + }, 35 + createdAt: { 36 + type: "string", 37 + description: "The date and time when the actor was created.", 38 + format: "datetime", 39 + }, 40 + updatedAt: { 41 + type: "string", 42 + description: "The date and time when the actor was last updated.", 43 + format: "datetime", 44 + }, 45 + }, 46 + }, 47 + }, 48 + }, 49 + IoPocketenvActorGetProfile: { 50 + lexicon: 1, 51 + id: "io.pocketenv.actor.getProfile", 52 + defs: { 53 + main: { 54 + type: "query", 55 + description: "Get the profile of an actor", 56 + parameters: { 57 + type: "params", 58 + properties: { 59 + did: { 60 + type: "string", 61 + description: "The DID or handle of the actor", 62 + format: "at-identifier", 63 + }, 64 + }, 65 + }, 66 + output: { 67 + encoding: "application/json", 68 + schema: { 69 + type: "ref", 70 + ref: "lex:io.pocketenv.actor.defs#profileViewDetailed", 71 + }, 72 + }, 73 + }, 74 + }, 75 + }, 7 76 AppBskyActorProfile: { 8 77 lexicon: 1, 9 78 id: "app.bsky.actor.profile", ··· 597 666 export const schemas = Object.values(schemaDict); 598 667 export const lexicons: Lexicons = new Lexicons(schemas); 599 668 export const ids = { 669 + IoPocketenvActorDefs: "io.pocketenv.actor.defs", 670 + IoPocketenvActorGetProfile: "io.pocketenv.actor.getProfile", 600 671 AppBskyActorProfile: "app.bsky.actor.profile", 601 672 IoPocketenvSandboxClaimSandbox: "io.pocketenv.sandbox.claimSandbox", 602 673 IoPocketenvSandboxCreateSandbox: "io.pocketenv.sandbox.createSandbox",
+37
apps/api/src/lexicon/types/io/pocketenv/actor/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + 9 + export interface ProfileViewDetailed { 10 + /** The unique identifier of the actor. */ 11 + id?: string; 12 + /** The DID of the actor. */ 13 + did?: string; 14 + /** The handle of the actor. */ 15 + handle?: string; 16 + /** The display name of the actor. */ 17 + displayName?: string; 18 + /** The URL of the actor's avatar image. */ 19 + avatar?: string; 20 + /** The date and time when the actor was created. */ 21 + createdAt?: string; 22 + /** The date and time when the actor was last updated. */ 23 + updatedAt?: string; 24 + [k: string]: unknown; 25 + } 26 + 27 + export function isProfileViewDetailed(v: unknown): v is ProfileViewDetailed { 28 + return ( 29 + isObj(v) && 30 + hasProp(v, "$type") && 31 + v.$type === "io.pocketenv.actor.defs#profileViewDetailed" 32 + ); 33 + } 34 + 35 + export function validateProfileViewDetailed(v: unknown): ValidationResult { 36 + return lexicons.validate("io.pocketenv.actor.defs#profileViewDetailed", v); 37 + }
+43
apps/api/src/lexicon/types/io/pocketenv/actor/getProfile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import type express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as IoPocketenvActorDefs from "./defs"; 11 + 12 + export interface QueryParams { 13 + /** The DID or handle of the actor */ 14 + did?: string; 15 + } 16 + 17 + export type InputSchema = undefined; 18 + export type OutputSchema = IoPocketenvActorDefs.ProfileViewDetailed; 19 + export type HandlerInput = undefined; 20 + 21 + export interface HandlerSuccess { 22 + encoding: "application/json"; 23 + body: OutputSchema; 24 + headers?: { [key: string]: string }; 25 + } 26 + 27 + export interface HandlerError { 28 + status: number; 29 + message?: string; 30 + } 31 + 32 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 33 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 34 + auth: HA; 35 + params: QueryParams; 36 + input: HandlerInput; 37 + req: express.Request; 38 + res: express.Response; 39 + resetRouteRateLimits: () => Promise<void>; 40 + }; 41 + export type Handler<HA extends HandlerAuth = never> = ( 42 + ctx: HandlerReqCtx<HA>, 43 + ) => Promise<HandlerOutput> | HandlerOutput;
+2
apps/api/src/xrpc/index.ts
··· 7 7 import startSandbox from "./io/pocketenv/sandbox/startSandbox"; 8 8 import stopSandbox from "./io/pocketenv/sandbox/stopSandbox"; 9 9 import claimSandbox from "./io/pocketenv/sandbox/claimSandbox"; 10 + import getProfile from "./io/pocketenv/actor/getProfile"; 10 11 11 12 export default function (server: Server, ctx: Context) { 12 13 // io.pocketenv ··· 17 18 startSandbox(server, ctx); 18 19 stopSandbox(server, ctx); 19 20 claimSandbox(server, ctx); 21 + getProfile(server, ctx); 20 22 21 23 return server; 22 24 }
+291
apps/api/src/xrpc/io/pocketenv/actor/getProfile.ts
··· 1 + import { type Agent, AtpAgent } from "@atproto/api"; 2 + import { consola } from "consola"; 3 + import type { OutputSchema } from "@atproto/api/dist/client/types/com/atproto/repo/getRecord"; 4 + import type { HandlerAuth } from "@atproto/xrpc-server"; 5 + import type { Context } from "context"; 6 + import { eq } from "drizzle-orm"; 7 + import { Effect, pipe } from "effect"; 8 + import type { Server } from "lexicon"; 9 + import type { ProfileViewDetailed } from "lexicon/types/io/pocketenv/actor/defs"; 10 + import type { QueryParams } from "lexicon/types/io/pocketenv/actor/getProfile"; 11 + import { createAgent } from "lib/agent"; 12 + import _ from "lodash"; 13 + import tables from "schema"; 14 + import type { SelectUser } from "schema/users"; 15 + 16 + export default function (server: Server, ctx: Context) { 17 + const getActorProfile = (params: QueryParams, auth: HandlerAuth) => 18 + pipe( 19 + { params, ctx, did: auth.credentials?.did }, 20 + resolveHandleToDid, 21 + Effect.flatMap(withServiceEndpoint), 22 + Effect.flatMap(withAgent), 23 + Effect.flatMap(withUser), 24 + Effect.flatMap(retrieveProfile), 25 + Effect.flatMap(refreshProfile), 26 + Effect.flatMap(presentation), 27 + Effect.retry({ times: 3 }), 28 + Effect.timeout("120 seconds"), 29 + Effect.catchAll((err) => { 30 + consola.error(err); 31 + return Effect.succeed({} as ProfileViewDetailed); 32 + }), 33 + ); 34 + server.io.pocketenv.actor.getProfile({ 35 + auth: ctx.authVerifier, 36 + handler: async ({ params, auth }) => { 37 + const result = await Effect.runPromise(getActorProfile(params, auth)); 38 + return { 39 + encoding: "application/json", 40 + body: result, 41 + }; 42 + }, 43 + }); 44 + } 45 + 46 + const resolveHandleToDid = ({ 47 + params, 48 + ctx, 49 + did, 50 + }: { 51 + params: QueryParams; 52 + ctx: Context; 53 + did?: string; 54 + }): Effect.Effect< 55 + { did?: string; ctx: Context; params: QueryParams }, 56 + Error 57 + > => { 58 + return Effect.tryPromise({ 59 + try: async () => { 60 + if (!params.did?.startsWith("did:plc:") && !!params.did) { 61 + return { 62 + did: await ctx.baseIdResolver.handle.resolve(params.did), 63 + ctx, 64 + params: { 65 + did: await ctx.baseIdResolver.handle.resolve(params.did), 66 + }, 67 + }; 68 + } 69 + return { 70 + did: params.did || did, 71 + ctx, 72 + params, 73 + }; 74 + }, 75 + catch: (error) => new Error(`Failed to resolve handle to DID: ${error}`), 76 + }); 77 + }; 78 + 79 + const withServiceEndpoint = ({ 80 + params, 81 + ctx, 82 + did, 83 + }: { 84 + params: QueryParams; 85 + ctx: Context; 86 + did?: string; 87 + }): Effect.Effect<WithServiceEndpoint, Error> => { 88 + return Effect.tryPromise({ 89 + try: async () => { 90 + if (params.did) { 91 + return fetch(`https://plc.directory/${params.did}`) 92 + .then((res) => res.json()) 93 + .then((data) => ({ 94 + did, 95 + serviceEndpoint: _.get(data, "service.0.serviceEndpoint"), 96 + ctx, 97 + params, 98 + })); 99 + } 100 + return { 101 + did, 102 + ctx, 103 + params, 104 + }; 105 + }, 106 + catch: (error) => new Error(`Failed to get service endpoint: ${error}`), 107 + }); 108 + }; 109 + 110 + const withAgent = ({ 111 + params, 112 + ctx, 113 + did, 114 + serviceEndpoint, 115 + }: WithServiceEndpoint): Effect.Effect<WithAgent, Error> => 116 + Effect.tryPromise({ 117 + try: async () => { 118 + return { 119 + ctx, 120 + did, 121 + params, 122 + agent: serviceEndpoint 123 + ? new AtpAgent({ service: serviceEndpoint }) 124 + : await createAgent(ctx.oauthClient, did!), 125 + }; 126 + }, 127 + catch: (error) => new Error(`Failed to create agent: ${error}`), 128 + }); 129 + 130 + const withUser = ({ 131 + params, 132 + ctx, 133 + did, 134 + agent, 135 + }: WithAgent): Effect.Effect<WithUser, Error> => { 136 + return Effect.tryPromise({ 137 + try: async () => { 138 + consola.info(">> did", did); 139 + return ctx.db 140 + .select() 141 + .from(tables.users) 142 + .where(eq(tables.users.did, did!)) 143 + .execute() 144 + .then((users) => ({ 145 + user: users[0], 146 + ctx, 147 + params, 148 + did, 149 + agent, 150 + })); 151 + }, 152 + catch: (error) => new Error(`Failed to retrieve current user: ${error}`), 153 + }); 154 + }; 155 + 156 + const retrieveProfile = ({ 157 + ctx, 158 + did, 159 + agent, 160 + user, 161 + }: WithUser): Effect.Effect<[Profile, string], Error> => { 162 + return Effect.tryPromise({ 163 + try: async () => { 164 + let record: OutputSchema | null = null; 165 + try { 166 + const { data } = await agent!.com.atproto.repo.getRecord({ 167 + repo: did!, 168 + collection: "app.bsky.actor.profile", 169 + rkey: "self", 170 + }); 171 + record = data; 172 + } catch (error) { 173 + consola.error("Failed to retrieve profile record:", error); 174 + } 175 + if (!record) { 176 + throw new Error("Profile record not found"); 177 + } 178 + 179 + const resolvedDid = did!; 180 + const handle = await ctx.resolver.resolveDidToHandle(resolvedDid); 181 + return [ 182 + { 183 + profileRecord: record, 184 + ctx, 185 + did: resolvedDid, 186 + user, 187 + }, 188 + handle, 189 + ] as [Profile, string]; 190 + }, 191 + catch: (error) => new Error(`Failed to retrieve profile: ${error}`), 192 + }); 193 + }; 194 + 195 + const refreshProfile = ([profile, handle]: [Profile, string]): Effect.Effect< 196 + [Profile, string], 197 + Error 198 + > => { 199 + return Effect.tryPromise({ 200 + try: async (): Promise<[Profile, string]> => { 201 + if (!profile.user) { 202 + await profile.ctx.db 203 + .insert(tables.users) 204 + .values({ 205 + did: profile.did, 206 + handle, 207 + avatar: `https://cdn.bsky.app/img/avatar/plain/${profile.did}/${_.get(profile, "profileRecord.value.avatar.ref", "").toString()}@jpeg`, 208 + displayName: _.get(profile, "profileRecord.value.displayName", ""), 209 + }) 210 + .execute(); 211 + const users = await profile.ctx.db 212 + .select() 213 + .from(tables.users) 214 + .where(eq(tables.users.did, profile.did)) 215 + .execute(); 216 + profile.user = users[0]; 217 + } else { 218 + // Update existing user in background if handle or avatar or displayName changed 219 + if ( 220 + profile.user.handle !== handle || 221 + profile.user.avatar !== 222 + `https://cdn.bsky.app/img/avatar/plain/${profile.did}/${_.get(profile, "profileRecord.value.avatar.ref", "").toString()}@jpeg` || 223 + profile.user.displayName !== 224 + _.get(profile, "profileRecord.value.displayName") 225 + ) { 226 + profile.ctx.db 227 + .update(tables.users) 228 + .set({ 229 + handle, 230 + avatar: `https://cdn.bsky.app/img/avatar/plain/${profile.did}/${_.get(profile, "profileRecord.value.avatar.ref", "").toString()}@jpeg`, 231 + displayName: _.get( 232 + profile, 233 + "profileRecord.value.displayName", 234 + "", 235 + ), 236 + updatedAt: new Date(), 237 + }) 238 + .where(eq(tables.users.id, profile.user.id)) 239 + .execute(); 240 + } 241 + } 242 + 243 + return [profile, handle] as [Profile, string]; 244 + }, 245 + catch: (error) => new Error(`Failed to refresh profile: ${error}`), 246 + }); 247 + }; 248 + 249 + const presentation = ([profile, handle]: [Profile, string]): Effect.Effect< 250 + ProfileViewDetailed, 251 + never 252 + > => { 253 + return Effect.sync(() => ({ 254 + id: profile.user?.id, 255 + did: profile.did, 256 + handle, 257 + displayName: _.get(profile, "profileRecord.value.displayName"), 258 + avatar: `https://cdn.bsky.app/img/avatar/plain/${profile.did}/${_.get(profile, "profileRecord.value.avatar.ref", "").toString()}@jpeg`, 259 + createdAt: profile.user?.createdAt.toISOString(), 260 + updatedAt: profile.user?.updatedAt.toISOString(), 261 + })); 262 + }; 263 + 264 + type Profile = { 265 + profileRecord: OutputSchema; 266 + ctx: Context; 267 + did: string; 268 + user?: SelectUser; 269 + }; 270 + 271 + type WithServiceEndpoint = { 272 + params: QueryParams; 273 + ctx: Context; 274 + did?: string; 275 + serviceEndpoint?: string; 276 + }; 277 + 278 + type WithAgent = { 279 + ctx: Context; 280 + did?: string; 281 + params: QueryParams; 282 + agent: Agent | AtpAgent | null; 283 + }; 284 + 285 + type WithUser = { 286 + user?: SelectUser; 287 + ctx: Context; 288 + params: QueryParams; 289 + did?: string; 290 + agent: Agent | AtpAgent | null; 291 + };
+12
apps/web/src/api/profile.ts
··· 1 + import { client } from "."; 2 + import type { Profile } from "../types/profile"; 3 + 4 + export const getCurrentProfile = () => 5 + client.get<Profile>(`/xrpc/io.pocketenv.actor.getProfile`, { 6 + headers: { 7 + Authorization: `Bearer ${localStorage.getItem("token")}`, 8 + }, 9 + }); 10 + 11 + export const getProfile = (did: string) => 12 + client.get<Profile>(`/xrpc/io.pocketenv.actor.getProfile?did=${did}`);
+19 -16
apps/web/src/components/navbar/Navbar.tsx
··· 5 5 import NewProject from "../newproject"; 6 6 import Logo from "../../assets/logo.png"; 7 7 import SignIn from "../signin"; 8 + import { useCurrentProfileQuery } from "../../hooks/useProfile"; 8 9 9 10 export type NavbarProps = { 10 11 title: string; ··· 18 19 const [signInModalOpen, setSignInModalOpen] = useState(false); 19 20 const dropdownRef = useRef<HTMLDivElement>(null); 20 21 const navigate = useNavigate(); 22 + const { data: profile, isLoading } = useCurrentProfileQuery(); 21 23 22 24 const toggleDropdown = () => setOpen(!open); 23 25 const toggleModal = () => { ··· 111 113 > 112 114 <div className="avatar avatar-placeholder"> 113 115 <div className="bg-secondary/10 w-10 rounded-full flex items-center justify-center"> 114 - {true && ( 115 - <img 116 - src="https://cdn.bsky.app/img/avatar/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreiebrezrvxt3istx4i4x3wqsfyle4shfetwq6nmlykoputyyqqe5ri@jpeg" 117 - alt="avatar 1" 118 - /> 116 + {profile?.avatar && ( 117 + <img src={profile.avatar} alt="avatar 1" /> 119 118 )} 120 - {false && ( 119 + {!profile?.avatar && ( 121 120 <span className="icon-[tabler--user] size-5 "></span> 122 121 )} 123 122 </div> ··· 134 133 <li className="dropdown-header gap-2"> 135 134 <div className="avatar"> 136 135 <div className="w-10 rounded-full"> 137 - <img 138 - src="https://cdn.bsky.app/img/avatar/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreiebrezrvxt3istx4i4x3wqsfyle4shfetwq6nmlykoputyyqqe5ri@jpeg" 139 - alt="avatar" 140 - /> 136 + {profile?.avatar && ( 137 + <img src={profile.avatar} alt="avatar 1" /> 138 + )} 139 + {!profile?.avatar && ( 140 + <span className="icon-[tabler--user] size-5 "></span> 141 + )} 141 142 </div> 142 143 </div> 143 144 <div> 144 - <h6 className="text-base-content text-base font-semibold"> 145 - Tsiry Sandratraina 146 - </h6> 145 + {profile?.displayName && ( 146 + <h6 className="text-base-content text-base font-semibold"> 147 + {profile.displayName} 148 + </h6> 149 + )} 147 150 <small className="text-base-content/50"> 148 - @tsiry-sandratraina.com 151 + @{profile?.handle} 149 152 </small> 150 153 </div> 151 154 </li> 152 155 <li> 153 - <a className="dropdown-item" href="/"> 156 + <Link className="dropdown-item" to="/projects"> 154 157 <span className="icon-[tabler--layout-dashboard]"></span> 155 158 Dashboard 156 - </a> 159 + </Link> 157 160 </li> 158 161 <li> 159 162 <Link className="dropdown-item" to="/settings">
+9
apps/web/src/hooks/useProfile.ts
··· 1 + import { useQuery } from "@tanstack/react-query"; 2 + import { getCurrentProfile } from "../api/profile"; 3 + 4 + export const useCurrentProfileQuery = () => 5 + useQuery({ 6 + queryKey: ["currentProfile"], 7 + queryFn: () => getCurrentProfile(), 8 + select: (response) => response.data, 9 + });
+9
apps/web/src/types/profile.ts
··· 1 + export type Profile = { 2 + id: string; 3 + did: string; 4 + handle: string; 5 + displayName: string; 6 + avatar: string; 7 + createdAt: string; 8 + updatedAt: string; 9 + };