···11-import type { Handle } from '@atcute/lexicons'
11+import type { Did, Handle } from '@atcute/lexicons'
22import {
33 getPublicAgent,
44 getPublicServiceAgent,
···1313import { getRelationship } from './follow.server'
1414import asyncWrap from './asyncWrap'
1515import { handleToDid } from './idResolver.server'
1616+import type { OAuthSession } from '@atproto/oauth-client-node'
1717+import type { AppBskyActorProfile } from '@atcute/bluesky'
16181719export async function getProfileData(request: Request, handle: Handle) {
1820 const [session] = await asyncWrap(() => getOAuthSession(request))
1919- const sessionAgent = session ? getSessionAgent(session) : getPublicAgent()
2021 const serviceDid =
2122 `did:web:${encodeURIComponent(new URL(env.API_URL).host)}` as const
2223 const serverClient = session
···3334 // Fetch all data in parallel
3435 const [feed, profile, wafrnProfiles, relationship] = await Promise.all([
3536 getFeed(session, handle),
3636- ok(
3737- sessionAgent.get('app.bsky.actor.getProfile', {
3838- params: { actor: profileDid }
3939- })
4040- ),
3737+ getProfile(session, profileDid),
4138 ok(
4239 serverClient.get('app.wafrn.actor.getProfiles', {
4340 params: { actors: [profileDid], includeCounts: true }
···6259 feed: feed ?? []
6360 }
6461}
6262+6363+export async function getProfile(session?: OAuthSession | null, did?: Did) {
6464+ const profileDid = did ?? session?.did
6565+ if (!profileDid) {
6666+ throw new Error('No DID provided for getProfile')
6767+ }
6868+6969+ const agent = session
7070+ ? getSessionAgent(session)
7171+ : getPublicServiceAgent(env.DEFAULT_PDS_URL as HTTPURL)
7272+ const profileReq = await agent.get('com.atproto.repo.getRecord', {
7373+ params: {
7474+ collection: 'app.bsky.actor.profile',
7575+ rkey: 'self',
7676+ repo: profileDid
7777+ }
7878+ })
7979+ if (!profileReq.ok) {
8080+ console.error(profileReq.data)
8181+ return null
8282+ }
8383+ return profileReq.data.value as AppBskyActorProfile.Main
8484+}
+6-29
packages/client/app/lib/user.server.ts
···11import { StatusError } from './https'
22-import { getSessionAgent, type XRPCLient } from '@www/lib/xrpcClient'
33-import type { Did } from '@atcute/lexicons'
22+import { getSessionAgent } from '@www/lib/xrpcClient'
43import { getOAuthSession } from './oauth.server'
55-import { didDocResolver } from './idResolver.server'
66-import { getAtprotoHandle } from '@atcute/identity'
44+import { ok } from '@atcute/client'
55+import { getProfile } from './profile.server'
7687export async function getCurrentUser(request: Request) {
98 try {
109 const session = await getOAuthSession(request)
1111- const client = getSessionAgent(session)
1212- const didDoc = await didDocResolver.resolve(session.did)
1313- const identity = {
1414- did: session.did,
1515- didDoc,
1616- handle: getAtprotoHandle(didDoc)
1717- }
1818- const profile = await getProfile(client, session.did)
1010+ const agent = getSessionAgent(session)
1111+ const identity = await ok(agent.get('com.atproto.server.getSession'))
1212+ const profile = await getProfile(session)
1913 return { profile, identity }
2014 } catch (error) {
2115 const isStatusError =
···3428 return null
3529 }
3630}
3737-3838-export async function getProfile(agent: XRPCLient, did: Did) {
3939- // if no 'app.bsky.actor.profile' record is found on the user's PDS, this method will not return an error
4040- // instead it will return an empty skeleton of a profile record with "handle" set to "handle.invalid"
4141- // const profile = await agent.getProfile(
4242- // didOrHandle ? { actor: didOrHandle } : undefined
4343- // )
4444- const profile = await agent.get('app.bsky.actor.getProfile', {
4545- params: { actor: did }
4646- })
4747- if (!profile.ok) {
4848- throw new Error('Failed to fetch profile')
4949- }
5050-5151- const data = profile.data.handle === 'handle.invalid' ? null : profile.data
5252- return data
5353-}
+21-1
packages/client/app/lib/xrpcClient.ts
···22 Client,
33 ok,
44 simpleFetchHandler,
55+ CredentialManager,
56 type FetchHandler
67} from '@atcute/client'
78···1112import type {} from '@atcute/atproto'
1213import type {} from '@watproto/lexicon'
13141414-import type { Did, Nsid } from '@atcute/lexicons'
1515+import type { Did, Handle, Nsid } from '@atcute/lexicons'
1516import { didWebToUrl, type OAuthSession } from '@atproto/oauth-client-node'
1617import { KeyserverClient } from '@atpkeyserver/client'
1718import { env } from './env.server'
18191920export type XRPCLient = Awaited<ReturnType<typeof getSessionAgent>>
2121+2222+/**
2323+ * This agent is what you need to use to edit your account settings like email, password, handle, etc.
2424+ * This has elevated privileges greater than the session agent, so use it with caution.
2525+ */
2626+export async function getLoginAgent(
2727+ serviceUrl: string,
2828+ handle: Handle,
2929+ password: string
3030+) {
3131+ const loginManager = new CredentialManager({
3232+ service: serviceUrl
3333+ })
3434+ await loginManager.login({
3535+ identifier: handle,
3636+ password
3737+ })
3838+ return new Client({ handler: loginManager })
3939+}
20402141export function getSessionAgent(session: OAuthSession) {
2242 const client = new Client({ handler: session.fetchHandler.bind(session) })
+15
packages/client/app/routes/oauth.$.tsx
···33import keys from '@www/jwks.json'
44import { commitSession, getSession } from '@www/lib/session.server'
55import { redirect } from 'react-router'
66+import { getServiceAgent, getSessionAgent } from '@www/lib/xrpcClient'
77+import { env } from '@www/lib/env.server'
88+import { ok } from '@atcute/client'
69710const publicKeys = keys.map((k) => {
811 const { kty, alg, kid, crv, x, y } = k
912 return { kty, alg, kid, crv, x, y }
1013})
1414+1515+const serviceDid =
1616+ `did:web:${encodeURIComponent(new URL(env.API_URL).host)}` as const
11171218export async function loader({ params, request }: Route.LoaderArgs) {
1319 const path = params['*']
···2733 type: 'success',
2834 message: 'You are now logged in'
2935 })
3636+ const { did, handle, status, active } = await ok(
3737+ getSessionAgent(session).get('com.atproto.server.getSession')
3838+ )
3939+ const agent = getServiceAgent(session, serviceDid)
4040+ await agent.post('app.wafrn.actor.cacheAccount', {
4141+ input: { did, handle, status, active }
4242+ })
4343+4444+ // call app.wafrn.actor.updateProfile with { last_login_at: Date.now() }
3045 const redirectUrl = new URL(state ?? '/', request.url).toString()
3146 return redirect(redirectUrl, {
3247 headers: {
+9-2
packages/client/app/routes/profile.$handle.tsx
···11import type { Route } from './+types/profile.$handle'
22-import type { Handle, Did, ResourceUri } from '@atcute/lexicons'
22+import type { Handle, Did, ResourceUri, Blob } from '@atcute/lexicons'
33import { Form, useLoaderData, useNavigation } from 'react-router'
44import PostFeed from '@www/components/PostFeed'
55import asyncWrap from '@www/lib/asyncWrap'
···8989 buttonVariant = 'btn-primary'
9090 }
91919292+ function formatAvatarBlob(blob: Blob) {
9393+ const blobId = blob.ref?.$link
9494+ return blobId
9595+ ? `https://cdn.bsky.app/img/avatar/plain/${did}/${blobId}@jpeg`
9696+ : ''
9797+ }
9898+9299 return (
93100 <div className="px-2 py-4 max-w-4xl mx-auto">
94101 {/* Profile Header */}
···97104 {/* Avatar */}
98105 {profile?.avatar && (
99106 <img
100100- src={profile.avatar}
107107+ src={formatAvatarBlob(profile.avatar as Blob<'image/png'>)}
101108 alt={profile.displayName || handle}
102109 className="w-20 h-20 rounded-full object-cover"
103110 />
···11+export * as AppWafrnActorCacheAccount from "./types/app/wafrn/actor/cacheAccount.js";
12export * as AppWafrnActorDefs from "./types/app/wafrn/actor/defs.js";
23export * as AppWafrnActorGetProfiles from "./types/app/wafrn/actor/getProfiles.js";
34export * as AppWafrnActorProfile from "./types/app/wafrn/actor/profile.js";
···22import type { Accounts } from '@api/db/schema'
33import type { Insertable } from 'kysely'
4455-export function createOrUpdateAccount(account: Insertable<Accounts>) {
55+export function cacheAccount(account: Insertable<Accounts>) {
66 return db
77 .insertInto('accounts')
88 .values([
+6-4
packages/server/src/lib/profile.ts
···77 * Creates the account if it doesn't exist, updates last_login_at if it does.
88 * This should be called on first interaction with any DID.
99 */
1010-export async function ensureAccount(did: string, handle: string): Promise<void> {
1010+export async function ensureAccount(
1111+ did: string,
1212+ handle: string
1313+): Promise<void> {
1114 await db
1215 .insertInto('accounts')
1316 .values({
···4548 server_origin: profile.server_origin ?? null,
4649 custom_fields: profile.custom_fields
4750 ? JSON.stringify(profile.custom_fields)
4848- : null,
4949- wafrn_updated_at: Date.now()
5151+ : null
5052 })
5153 .onConflict((oc) =>
5254 oc.column('did').doUpdateSet({
···5557 custom_fields: profile.custom_fields
5658 ? JSON.stringify(profile.custom_fields)
5759 : null,
5858- wafrn_updated_at: Date.now()
6060+ updated_at: Date.now()
5961 })
6062 )
6163 .execute()