diff options
Diffstat (limited to 'src/state/session/agent.ts')
-rw-r--r-- | src/state/session/agent.ts | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts new file mode 100644 index 000000000..ab7ebc790 --- /dev/null +++ b/src/state/session/agent.ts @@ -0,0 +1,190 @@ +import {BskyAgent} from '@atproto/api' +import {AtpSessionEvent} from '@atproto-labs/api' + +import {networkRetry} from '#/lib/async/retry' +import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {tryFetchGates} from '#/lib/statsig/statsig' +import { + configureModerationForAccount, + configureModerationForGuest, +} from './moderation' +import {SessionAccount} from './types' +import {isSessionDeactivated, isSessionExpired} from './util' +import {IS_PROD_SERVICE} from '#/lib/constants' +import {DEFAULT_PROD_FEEDS} from '../queries/preferences' + +export function createPublicAgent() { + configureModerationForGuest() // Side effect but only relevant for tests + return new BskyAgent({service: PUBLIC_BSKY_SERVICE}) +} + +export async function createAgentAndResume( + storedAccount: SessionAccount, + onSessionChange: ( + agent: BskyAgent, + did: string, + event: AtpSessionEvent, + ) => void, +) { + const agent = new BskyAgent({service: storedAccount.service}) + if (storedAccount.pdsUrl) { + agent.pdsUrl = agent.api.xrpc.uri = new URL(storedAccount.pdsUrl) + } + const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency') + const moderation = configureModerationForAccount(agent, storedAccount) + const prevSession = { + accessJwt: storedAccount.accessJwt ?? '', + refreshJwt: storedAccount.refreshJwt ?? '', + did: storedAccount.did, + handle: storedAccount.handle, + } + if (isSessionExpired(storedAccount)) { + await networkRetry(1, () => agent.resumeSession(prevSession)) + } else { + agent.session = prevSession + if (!storedAccount.deactivated) { + // Intentionally not awaited to unblock the UI: + networkRetry(1, () => agent.resumeSession(prevSession)) + } + } + + return prepareAgent(agent, gates, moderation, onSessionChange) +} + +export async function createAgentAndLogin( + { + service, + identifier, + password, + authFactorToken, + }: { + service: string + identifier: string + password: string + authFactorToken?: string + }, + onSessionChange: ( + agent: BskyAgent, + did: string, + event: AtpSessionEvent, + ) => void, +) { + const agent = new BskyAgent({service}) + await agent.login({identifier, password, authFactorToken}) + + const account = agentToSessionAccountOrThrow(agent) + const gates = tryFetchGates(account.did, 'prefer-fresh-gates') + const moderation = configureModerationForAccount(agent, account) + return prepareAgent(agent, moderation, gates, onSessionChange) +} + +export async function createAgentAndCreateAccount( + { + service, + email, + password, + handle, + birthDate, + inviteCode, + verificationPhone, + verificationCode, + }: { + service: string + email: string + password: string + handle: string + birthDate: Date + inviteCode?: string + verificationPhone?: string + verificationCode?: string + }, + onSessionChange: ( + agent: BskyAgent, + did: string, + event: AtpSessionEvent, + ) => void, +) { + const agent = new BskyAgent({service}) + await agent.createAccount({ + email, + password, + handle, + inviteCode, + verificationPhone, + verificationCode, + }) + const account = agentToSessionAccountOrThrow(agent) + const gates = tryFetchGates(account.did, 'prefer-fresh-gates') + const moderation = configureModerationForAccount(agent, account) + if (!account.deactivated) { + /*dont await*/ agent.upsertProfile(_existing => { + return { + displayName: '', + // HACKFIX + // creating a bunch of identical profile objects is breaking the relay + // tossing this unspecced field onto it to reduce the size of the problem + // -prf + createdAt: new Date().toISOString(), + } + }) + } + + // Not awaited so that we can still get into onboarding. + // This is OK because we won't let you toggle adult stuff until you set the date. + agent.setPersonalDetails({birthDate: birthDate.toISOString()}) + if (IS_PROD_SERVICE(service)) { + agent.setSavedFeeds(DEFAULT_PROD_FEEDS.saved, DEFAULT_PROD_FEEDS.pinned) + } + + return prepareAgent(agent, gates, moderation, onSessionChange) +} + +async function prepareAgent( + agent: BskyAgent, + // Not awaited in the calling code so we can delay blocking on them. + gates: Promise<void>, + moderation: Promise<void>, + onSessionChange: ( + agent: BskyAgent, + did: string, + event: AtpSessionEvent, + ) => void, +) { + // There's nothing else left to do, so block on them here. + await Promise.all([gates, moderation]) + + // Now the agent is ready. + const account = agentToSessionAccountOrThrow(agent) + agent.setPersistSessionHandler(event => { + onSessionChange(agent, account.did, event) + }) + return {agent, account} +} + +export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount { + const account = agentToSessionAccount(agent) + if (!account) { + throw Error('Expected an active session') + } + return account +} + +export function agentToSessionAccount( + agent: BskyAgent, +): SessionAccount | undefined { + if (!agent.session) { + return undefined + } + return { + service: agent.service.toString(), + did: agent.session.did, + handle: agent.session.handle, + email: agent.session.email, + emailConfirmed: agent.session.emailConfirmed || false, + emailAuthFactor: agent.session.emailAuthFactor || false, + refreshJwt: agent.session.refreshJwt, + accessJwt: agent.session.accessJwt, + deactivated: isSessionDeactivated(agent.session.accessJwt), + pdsUrl: agent.pdsUrl?.toString(), + } +} |