diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.native.tsx | 2 | ||||
-rw-r--r-- | src/App.web.tsx | 2 | ||||
-rw-r--r-- | src/state/queries/index.ts | 8 | ||||
-rw-r--r-- | src/state/session/index.tsx | 473 | ||||
-rw-r--r-- | src/state/session/types.ts | 65 | ||||
-rw-r--r-- | src/state/session/util/index.ts | 177 | ||||
-rw-r--r-- | src/state/session/util/readLastActiveAccount.ts | 6 |
7 files changed, 400 insertions, 333 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index 4cb963fe8..569f342c2 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -18,7 +18,7 @@ import {useQueryClient} from '@tanstack/react-query' import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {init as initPersistedState} from '#/state/persisted' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' -import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount' +import {readLastActiveAccount} from '#/state/session/util' import {useIntentHandler} from 'lib/hooks/useIntentHandler' import {useNotificationsListener} from 'lib/notifications/notifications' import {QueryProvider} from 'lib/react-query' diff --git a/src/App.web.tsx b/src/App.web.tsx index 639fbfafc..226fb8d3f 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -8,7 +8,7 @@ import {SafeAreaProvider} from 'react-native-safe-area-context' import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {init as initPersistedState} from '#/state/persisted' import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs' -import {readLastActiveAccount} from '#/state/session/util/readLastActiveAccount' +import {readLastActiveAccount} from '#/state/session/util' import {useIntentHandler} from 'lib/hooks/useIntentHandler' import {QueryProvider} from 'lib/react-query' import {ThemeProvider} from 'lib/ThemeContext' diff --git a/src/state/queries/index.ts b/src/state/queries/index.ts index e30528ca1..0635bf316 100644 --- a/src/state/queries/index.ts +++ b/src/state/queries/index.ts @@ -1,11 +1,3 @@ -import {BskyAgent} from '@atproto/api' - -import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' - -export const PUBLIC_BSKY_AGENT = new BskyAgent({ - service: PUBLIC_BSKY_SERVICE, -}) - export const STALE = { SECONDS: { FIFTEEN: 1e3 * 15, diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 6cdfa145d..6173f8cca 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -1,100 +1,41 @@ import React from 'react' -import { - AtpPersistSessionHandler, - BSKY_LABELER_DID, - BskyAgent, -} from '@atproto/api' -import {jwtDecode} from 'jwt-decode' +import {AtpPersistSessionHandler, BskyAgent} from '@atproto/api' import {track} from '#/lib/analytics/analytics' import {networkRetry} from '#/lib/async/retry' -import {IS_TEST_USER} from '#/lib/constants' -import {logEvent, LogEvents, tryFetchGates} from '#/lib/statsig/statsig' -import {hasProp} from '#/lib/type-guards' +import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {logEvent, tryFetchGates} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' -import {PUBLIC_BSKY_AGENT} from '#/state/queries' import {useCloseAllActiveElements} from '#/state/util' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {IS_DEV} from '#/env' import {emitSessionDropped} from '../events' -import {readLabelers} from './agent-config' - -let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT - -function __getAgent() { - return __globalAgent -} - -export function useAgent() { - return React.useMemo(() => ({getAgent: __getAgent}), []) -} +import { + agentToSessionAccount, + configureModerationForAccount, + configureModerationForGuest, + createAgentAndCreateAccount, + createAgentAndLogin, + isSessionDeactivated, + isSessionExpired, +} from './util' + +export type {SessionAccount} from '#/state/session/types' +import { + SessionAccount, + SessionApiContext, + SessionState, + SessionStateContext, +} from '#/state/session/types' -export type SessionAccount = persisted.PersistedAccount +export {isSessionDeactivated} -export type SessionState = { - isInitialLoad: boolean - isSwitchingAccounts: boolean - accounts: SessionAccount[] - currentAccount: SessionAccount | undefined -} -export type StateContext = SessionState & { - hasSession: boolean -} -export type ApiContext = { - createAccount: (props: { - service: string - email: string - password: string - handle: string - inviteCode?: string - verificationPhone?: string - verificationCode?: string - }) => Promise<void> - login: ( - props: { - service: string - identifier: string - password: string - authFactorToken?: string | undefined - }, - logContext: LogEvents['account:loggedIn']['logContext'], - ) => Promise<void> - /** - * A full logout. Clears the `currentAccount` from session, AND removes - * access tokens from all accounts, so that returning as any user will - * require a full login. - */ - logout: ( - logContext: LogEvents['account:loggedOut']['logContext'], - ) => Promise<void> - /** - * A partial logout. Clears the `currentAccount` from session, but DOES NOT - * clear access tokens from accounts, allowing the user to return to their - * other accounts without logging in. - * - * Used when adding a new account, deleting an account. - */ - clearCurrentAccount: () => void - initSession: (account: SessionAccount) => Promise<void> - resumeSession: (account?: SessionAccount) => Promise<void> - removeAccount: (account: SessionAccount) => void - selectAccount: ( - account: SessionAccount, - logContext: LogEvents['account:loggedIn']['logContext'], - ) => Promise<void> - updateCurrentAccount: ( - account: Partial< - Pick< - SessionAccount, - 'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor' - > - >, - ) => void -} +const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE}) +configureModerationForGuest() -const StateContext = React.createContext<StateContext>({ +const StateContext = React.createContext<SessionStateContext>({ isInitialLoad: true, isSwitchingAccounts: false, accounts: [], @@ -102,7 +43,7 @@ const StateContext = React.createContext<StateContext>({ hasSession: false, }) -const ApiContext = React.createContext<ApiContext>({ +const ApiContext = React.createContext<SessionApiContext>({ createAccount: async () => {}, login: async () => {}, logout: async () => {}, @@ -114,71 +55,10 @@ const ApiContext = React.createContext<ApiContext>({ clearCurrentAccount: () => {}, }) -function createPersistSessionHandler( - agent: BskyAgent, - account: SessionAccount, - persistSessionCallback: (props: { - expired: boolean - refreshedAccount: SessionAccount - }) => void, - { - networkErrorCallback, - }: { - networkErrorCallback?: () => void - } = {}, -): AtpPersistSessionHandler { - return function persistSession(event, session) { - const expired = event === 'expired' || event === 'create-failed' - - if (event === 'network-error') { - logger.warn(`session: persistSessionHandler received network-error event`) - networkErrorCallback?.() - return - } - - const refreshedAccount: SessionAccount = { - service: account.service, - did: session?.did || account.did, - handle: session?.handle || account.handle, - email: session?.email || account.email, - emailConfirmed: session?.emailConfirmed || account.emailConfirmed, - emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor, - deactivated: isSessionDeactivated(session?.accessJwt), - pdsUrl: agent.pdsUrl?.toString(), - - /* - * Tokens are undefined if the session expires, or if creation fails for - * any reason e.g. tokens are invalid, network error, etc. - */ - refreshJwt: session?.refreshJwt, - accessJwt: session?.accessJwt, - } - - logger.debug(`session: persistSession`, { - event, - deactivated: refreshedAccount.deactivated, - }) - - if (expired) { - logger.warn(`session: expired`) - emitSessionDropped() - } +let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT - /* - * If the session expired, or it was successfully created/updated, we want - * to update/persist the data. - * - * If the session creation failed, it could be a network error, or it could - * be more serious like an invalid token(s). We can't differentiate, so in - * order to allow the user to get a fresh token (if they need it), we need - * to persist this data and wipe their tokens, effectively logging them - * out. - */ - persistSessionCallback({ - expired, - refreshedAccount, - }) - } +function __getAgent() { + return __globalAgent } export function Provider({children}: React.PropsWithChildren<{}>) { @@ -214,13 +94,87 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const clearCurrentAccount = React.useCallback(() => { logger.warn(`session: clear current account`) __globalAgent = PUBLIC_BSKY_AGENT + configureModerationForGuest() setStateAndPersist(s => ({ ...s, currentAccount: undefined, })) }, [setStateAndPersist]) - const createAccount = React.useCallback<ApiContext['createAccount']>( + const createPersistSessionHandler = React.useCallback( + ( + agent: BskyAgent, + account: SessionAccount, + persistSessionCallback: (props: { + expired: boolean + refreshedAccount: SessionAccount + }) => void, + { + networkErrorCallback, + }: { + networkErrorCallback?: () => void + } = {}, + ): AtpPersistSessionHandler => { + return function persistSession(event, session) { + const expired = event === 'expired' || event === 'create-failed' + + if (event === 'network-error') { + logger.warn( + `session: persistSessionHandler received network-error event`, + ) + networkErrorCallback?.() + return + } + + // TODO: use agentToSessionAccount for this too. + const refreshedAccount: SessionAccount = { + service: account.service, + did: session?.did || account.did, + handle: session?.handle || account.handle, + email: session?.email || account.email, + emailConfirmed: session?.emailConfirmed || account.emailConfirmed, + emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor, + deactivated: isSessionDeactivated(session?.accessJwt), + pdsUrl: agent.pdsUrl?.toString(), + + /* + * Tokens are undefined if the session expires, or if creation fails for + * any reason e.g. tokens are invalid, network error, etc. + */ + refreshJwt: session?.refreshJwt, + accessJwt: session?.accessJwt, + } + + logger.debug(`session: persistSession`, { + event, + deactivated: refreshedAccount.deactivated, + }) + + if (expired) { + logger.warn(`session: expired`) + emitSessionDropped() + } + + /* + * If the session expired, or it was successfully created/updated, we want + * to update/persist the data. + * + * If the session creation failed, it could be a network error, or it could + * be more serious like an invalid token(s). We can't differentiate, so in + * order to allow the user to get a fresh token (if they need it), we need + * to persist this data and wipe their tokens, effectively logging them + * out. + */ + persistSessionCallback({ + expired, + refreshedAccount, + }) + } + }, + [], + ) + + const createAccount = React.useCallback<SessionApiContext['createAccount']>( async ({ service, email, @@ -229,60 +183,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) { inviteCode, verificationPhone, verificationCode, - }: any) => { + }) => { logger.info(`session: creating account`) track('Try Create Account') logEvent('account:create:begin', {}) - - const agent = new BskyAgent({service}) - - await agent.createAccount({ - handle, - password, - email, - inviteCode, - verificationPhone, - verificationCode, - }) - - if (!agent.session) { - throw new Error(`session: createAccount failed to establish a session`) - } - const fetchingGates = tryFetchGates( - agent.session.did, - 'prefer-fresh-gates', + const {agent, account, fetchingGates} = await createAgentAndCreateAccount( + { + service, + email, + password, + handle, + inviteCode, + verificationPhone, + verificationCode, + }, ) - const deactivated = isSessionDeactivated(agent.session.accessJwt) - if (!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(), - } - }) - } - - const account: SessionAccount = { - service: agent.service.toString(), - did: agent.session.did, - handle: agent.session.handle, - email: agent.session.email, - emailConfirmed: agent.session.emailConfirmed, - emailAuthFactor: agent.session.emailAuthFactor, - refreshJwt: agent.session.refreshJwt, - accessJwt: agent.session.accessJwt, - deactivated, - pdsUrl: agent.pdsUrl?.toString(), - } - - await configureModeration(agent, account) - agent.setPersistSessionHandler( createPersistSessionHandler( agent, @@ -302,39 +218,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) { track('Create Account') logEvent('account:create:success', {}) }, - [upsertAccount, clearCurrentAccount], + [upsertAccount, clearCurrentAccount, createPersistSessionHandler], ) - const login = React.useCallback<ApiContext['login']>( + const login = React.useCallback<SessionApiContext['login']>( async ({service, identifier, password, authFactorToken}, logContext) => { logger.debug(`session: login`, {}, logger.DebugContext.session) - - const agent = new BskyAgent({service}) - - await agent.login({identifier, password, authFactorToken}) - - if (!agent.session) { - throw new Error(`session: login failed to establish a session`) - } - const fetchingGates = tryFetchGates( - agent.session.did, - 'prefer-fresh-gates', - ) - - const account: SessionAccount = { - service: agent.service.toString(), - did: agent.session.did, - handle: agent.session.handle, - email: agent.session.email, - emailConfirmed: agent.session.emailConfirmed, - emailAuthFactor: agent.session.emailAuthFactor, - refreshJwt: agent.session.refreshJwt, - accessJwt: agent.session.accessJwt, - deactivated: isSessionDeactivated(agent.session.accessJwt), - pdsUrl: agent.pdsUrl?.toString(), - } - - await configureModeration(agent, account) + const {agent, account, fetchingGates} = await createAgentAndLogin({ + service, + identifier, + password, + authFactorToken, + }) agent.setPersistSessionHandler( createPersistSessionHandler( @@ -358,10 +253,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) { track('Sign In', {resumedSession: false}) logEvent('account:loggedIn', {logContext, withPassword: true}) }, - [upsertAccount, clearCurrentAccount], + [upsertAccount, clearCurrentAccount, createPersistSessionHandler], ) - const logout = React.useCallback<ApiContext['logout']>( + const logout = React.useCallback<SessionApiContext['logout']>( async logContext => { logger.debug(`session: logout`) clearCurrentAccount() @@ -380,7 +275,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [clearCurrentAccount, setStateAndPersist], ) - const initSession = React.useCallback<ApiContext['initSession']>( + const initSession = React.useCallback<SessionApiContext['initSession']>( async account => { logger.debug(`session: initSession`, {}, logger.DebugContext.session) const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency') @@ -405,22 +300,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // @ts-ignore if (IS_DEV && isWeb) window.agent = agent - await configureModeration(agent, account) - - let canReusePrevSession = false - try { - if (account.accessJwt) { - const decoded = jwtDecode(account.accessJwt) - if (decoded.exp) { - const didExpire = Date.now() >= decoded.exp * 1000 - if (!didExpire) { - canReusePrevSession = true - } - } - } - } catch (e) { - logger.error(`session: could not decode jwt`) - } + await configureModerationForAccount(agent, account) const accountOrSessionDeactivated = isSessionDeactivated(account.accessJwt) || account.deactivated @@ -432,7 +312,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) { handle: account.handle, } - if (canReusePrevSession) { + if (isSessionExpired(account)) { + logger.debug(`session: attempting to resume using previous session`) + + try { + const freshAccount = await resumeSessionWithFreshAccount() + __globalAgent = agent + await fetchingGates + upsertAccount(freshAccount) + } catch (e) { + /* + * Note: `agent.persistSession` is also called when this fails, and + * we handle that failure via `createPersistSessionHandler` + */ + logger.info(`session: resumeSessionWithFreshAccount failed`, { + message: e, + }) + + __globalAgent = PUBLIC_BSKY_AGENT + } + } else { logger.debug(`session: attempting to reuse previous session`) agent.session = prevSession @@ -469,59 +368,27 @@ export function Provider({children}: React.PropsWithChildren<{}>) { __globalAgent = PUBLIC_BSKY_AGENT }) - } else { - logger.debug(`session: attempting to resume using previous session`) - - try { - const freshAccount = await resumeSessionWithFreshAccount() - __globalAgent = agent - await fetchingGates - upsertAccount(freshAccount) - } catch (e) { - /* - * Note: `agent.persistSession` is also called when this fails, and - * we handle that failure via `createPersistSessionHandler` - */ - logger.info(`session: resumeSessionWithFreshAccount failed`, { - message: e, - }) - - __globalAgent = PUBLIC_BSKY_AGENT - } } async function resumeSessionWithFreshAccount(): Promise<SessionAccount> { logger.debug(`session: resumeSessionWithFreshAccount`) await networkRetry(1, () => agent.resumeSession(prevSession)) - + const sessionAccount = agentToSessionAccount(agent) /* * If `agent.resumeSession` fails above, it'll throw. This is just to * make TypeScript happy. */ - if (!agent.session) { + if (!sessionAccount) { throw new Error(`session: initSession failed to establish a session`) } - - // ensure changes in handle/email etc are captured on reload - return { - service: agent.service.toString(), - did: agent.session.did, - handle: agent.session.handle, - email: agent.session.email, - emailConfirmed: agent.session.emailConfirmed, - emailAuthFactor: agent.session.emailAuthFactor, - refreshJwt: agent.session.refreshJwt, - accessJwt: agent.session.accessJwt, - deactivated: isSessionDeactivated(agent.session.accessJwt), - pdsUrl: agent.pdsUrl?.toString(), - } + return sessionAccount } }, - [upsertAccount, clearCurrentAccount], + [upsertAccount, clearCurrentAccount, createPersistSessionHandler], ) - const resumeSession = React.useCallback<ApiContext['resumeSession']>( + const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( async account => { try { if (account) { @@ -539,7 +406,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [initSession], ) - const removeAccount = React.useCallback<ApiContext['removeAccount']>( + const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( account => { setStateAndPersist(s => { return { @@ -552,7 +419,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const updateCurrentAccount = React.useCallback< - ApiContext['updateCurrentAccount'] + SessionApiContext['updateCurrentAccount'] >( account => { setStateAndPersist(s => { @@ -588,7 +455,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [setStateAndPersist], ) - const selectAccount = React.useCallback<ApiContext['selectAccount']>( + const selectAccount = React.useCallback<SessionApiContext['selectAccount']>( async (account, logContext) => { setState(s => ({...s, isSwitchingAccounts: true})) try { @@ -714,28 +581,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) } -async function configureModeration(agent: BskyAgent, account: SessionAccount) { - if (IS_TEST_USER(account.handle)) { - const did = ( - await agent - .resolveHandle({handle: 'mod-authority.test'}) - .catch(_ => undefined) - )?.data.did - if (did) { - console.warn('USING TEST ENV MODERATION') - BskyAgent.configure({appLabelers: [did]}) - } - } else { - BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) - const labelerDids = await readLabelers(account.did).catch(_ => {}) - if (labelerDids) { - agent.configureLabelersHeader( - labelerDids.filter(did => did !== BSKY_LABELER_DID), - ) - } - } -} - export function useSession() { return React.useContext(StateContext) } @@ -762,12 +607,6 @@ export function useRequireAuth() { ) } -export function isSessionDeactivated(accessJwt: string | undefined) { - if (accessJwt) { - const sessData = jwtDecode(accessJwt) - return ( - hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated' - ) - } - return false +export function useAgent() { + return React.useMemo(() => ({getAgent: __getAgent}), []) } diff --git a/src/state/session/types.ts b/src/state/session/types.ts new file mode 100644 index 000000000..3c7e7d253 --- /dev/null +++ b/src/state/session/types.ts @@ -0,0 +1,65 @@ +import {LogEvents} from '#/lib/statsig/statsig' +import {PersistedAccount} from '#/state/persisted' + +export type SessionAccount = PersistedAccount + +export type SessionState = { + isInitialLoad: boolean + isSwitchingAccounts: boolean + accounts: SessionAccount[] + currentAccount: SessionAccount | undefined +} +export type SessionStateContext = SessionState & { + hasSession: boolean +} +export type SessionApiContext = { + createAccount: (props: { + service: string + email: string + password: string + handle: string + inviteCode?: string + verificationPhone?: string + verificationCode?: string + }) => Promise<void> + login: ( + props: { + service: string + identifier: string + password: string + authFactorToken?: string | undefined + }, + logContext: LogEvents['account:loggedIn']['logContext'], + ) => Promise<void> + /** + * A full logout. Clears the `currentAccount` from session, AND removes + * access tokens from all accounts, so that returning as any user will + * require a full login. + */ + logout: ( + logContext: LogEvents['account:loggedOut']['logContext'], + ) => Promise<void> + /** + * A partial logout. Clears the `currentAccount` from session, but DOES NOT + * clear access tokens from accounts, allowing the user to return to their + * other accounts without logging in. + * + * Used when adding a new account, deleting an account. + */ + clearCurrentAccount: () => void + initSession: (account: SessionAccount) => Promise<void> + resumeSession: (account?: SessionAccount) => Promise<void> + removeAccount: (account: SessionAccount) => void + selectAccount: ( + account: SessionAccount, + logContext: LogEvents['account:loggedIn']['logContext'], + ) => Promise<void> + updateCurrentAccount: ( + account: Partial< + Pick< + SessionAccount, + 'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor' + > + >, + ) => void +} diff --git a/src/state/session/util/index.ts b/src/state/session/util/index.ts new file mode 100644 index 000000000..e3e246f7b --- /dev/null +++ b/src/state/session/util/index.ts @@ -0,0 +1,177 @@ +import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api' +import {jwtDecode} from 'jwt-decode' + +import {IS_TEST_USER} from '#/lib/constants' +import {tryFetchGates} from '#/lib/statsig/statsig' +import {hasProp} from '#/lib/type-guards' +import {logger} from '#/logger' +import * as persisted from '#/state/persisted' +import {readLabelers} from '../agent-config' +import {SessionAccount, SessionApiContext} from '../types' + +export function isSessionDeactivated(accessJwt: string | undefined) { + if (accessJwt) { + const sessData = jwtDecode(accessJwt) + return ( + hasProp(sessData, 'scope') && sessData.scope === 'com.atproto.deactivated' + ) + } + return false +} + +export function readLastActiveAccount() { + const {currentAccount, accounts} = persisted.get('session') + return accounts.find(a => a.did === currentAccount?.did) +} + +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(), + } +} + +export function configureModerationForGuest() { + switchToBskyAppLabeler() +} + +export async function configureModerationForAccount( + agent: BskyAgent, + account: SessionAccount, +) { + switchToBskyAppLabeler() + if (IS_TEST_USER(account.handle)) { + await trySwitchToTestAppLabeler(agent) + } + + const labelerDids = await readLabelers(account.did).catch(_ => {}) + if (labelerDids) { + agent.configureLabelersHeader( + labelerDids.filter(did => did !== BSKY_LABELER_DID), + ) + } else { + // If there are no headers in the storage, we'll not send them on the initial requests. + // If we wanted to fix this, we could block on the preferences query here. + } +} + +function switchToBskyAppLabeler() { + BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) +} + +async function trySwitchToTestAppLabeler(agent: BskyAgent) { + const did = ( + await agent + .resolveHandle({handle: 'mod-authority.test'}) + .catch(_ => undefined) + )?.data.did + if (did) { + console.warn('USING TEST ENV MODERATION') + BskyAgent.configure({appLabelers: [did]}) + } +} + +export function isSessionExpired(account: SessionAccount) { + try { + if (account.accessJwt) { + const decoded = jwtDecode(account.accessJwt) + if (decoded.exp) { + const didExpire = Date.now() >= decoded.exp * 1000 + return didExpire + } + } + } catch (e) { + logger.error(`session: could not decode jwt`) + } + return true +} + +export async function createAgentAndLogin({ + service, + identifier, + password, + authFactorToken, +}: { + service: string + identifier: string + password: string + authFactorToken?: string +}) { + const agent = new BskyAgent({service}) + await agent.login({identifier, password, authFactorToken}) + + const account = agentToSessionAccount(agent) + if (!agent.session || !account) { + throw new Error(`session: login failed to establish a session`) + } + + const fetchingGates = tryFetchGates(account.did, 'prefer-fresh-gates') + await configureModerationForAccount(agent, account) + + return { + agent, + account, + fetchingGates, + } +} + +export async function createAgentAndCreateAccount({ + service, + email, + password, + handle, + inviteCode, + verificationPhone, + verificationCode, +}: Parameters<SessionApiContext['createAccount']>[0]) { + const agent = new BskyAgent({service}) + await agent.createAccount({ + email, + password, + handle, + inviteCode, + verificationPhone, + verificationCode, + }) + + const account = agentToSessionAccount(agent)! + if (!agent.session || !account) { + throw new Error(`session: createAccount failed to establish a session`) + } + + const fetchingGates = tryFetchGates(account.did, 'prefer-fresh-gates') + + 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(), + } + }) + } + + await configureModerationForAccount(agent, account) + + return { + agent, + account, + fetchingGates, + } +} diff --git a/src/state/session/util/readLastActiveAccount.ts b/src/state/session/util/readLastActiveAccount.ts deleted file mode 100644 index e0768b8a8..000000000 --- a/src/state/session/util/readLastActiveAccount.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as persisted from '#/state/persisted' - -export function readLastActiveAccount() { - const {currentAccount, accounts} = persisted.get('session') - return accounts.find(a => a.did === currentAccount?.did) -} |