about summary refs log tree commit diff
path: root/src/state/session/agent.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/session/agent.ts')
-rw-r--r--src/state/session/agent.ts190
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(),
+  }
+}