mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 8.6 kB view raw
1import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api' 2import {TID} from '@atproto/common-web' 3 4import {networkRetry} from '#/lib/async/retry' 5import { 6 DISCOVER_SAVED_FEED, 7 IS_PROD_SERVICE, 8 PUBLIC_BSKY_SERVICE, 9 TIMELINE_SAVED_FEED, 10} from '#/lib/constants' 11import {tryFetchGates} from '#/lib/statsig/statsig' 12import {getAge} from '#/lib/strings/time' 13import {logger} from '#/logger' 14import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' 15import {emitNetworkConfirmed, emitNetworkLost} from '../events' 16import {addSessionErrorLog} from './logging' 17import { 18 configureModerationForAccount, 19 configureModerationForGuest, 20} from './moderation' 21import {SessionAccount} from './types' 22import {isSessionExpired, isSignupQueued} from './util' 23 24export function createPublicAgent() { 25 configureModerationForGuest() // Side effect but only relevant for tests 26 return new BskyAppAgent({service: PUBLIC_BSKY_SERVICE}) 27} 28 29export async function createAgentAndResume( 30 storedAccount: SessionAccount, 31 onSessionChange: ( 32 agent: BskyAgent, 33 did: string, 34 event: AtpSessionEvent, 35 ) => void, 36) { 37 const agent = new BskyAppAgent({service: storedAccount.service}) 38 if (storedAccount.pdsUrl) { 39 agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl) 40 } 41 const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency') 42 const moderation = configureModerationForAccount(agent, storedAccount) 43 const prevSession: AtpSessionData = sessionAccountToSession(storedAccount) 44 if (isSessionExpired(storedAccount)) { 45 await networkRetry(1, () => agent.resumeSession(prevSession)) 46 } else { 47 agent.sessionManager.session = prevSession 48 if (!storedAccount.signupQueued) { 49 networkRetry(3, () => agent.resumeSession(prevSession)).catch( 50 (e: any) => { 51 logger.error(`networkRetry failed to resume session`, { 52 status: e?.status || 'unknown', 53 // this field name is ignored by Sentry scrubbers 54 safeMessage: e?.message || 'unknown', 55 }) 56 57 throw e 58 }, 59 ) 60 } 61 } 62 63 return agent.prepare(gates, moderation, onSessionChange) 64} 65 66export async function createAgentAndLogin( 67 { 68 service, 69 identifier, 70 password, 71 authFactorToken, 72 }: { 73 service: string 74 identifier: string 75 password: string 76 authFactorToken?: string 77 }, 78 onSessionChange: ( 79 agent: BskyAgent, 80 did: string, 81 event: AtpSessionEvent, 82 ) => void, 83) { 84 const agent = new BskyAppAgent({service}) 85 await agent.login({identifier, password, authFactorToken}) 86 87 const account = agentToSessionAccountOrThrow(agent) 88 const gates = tryFetchGates(account.did, 'prefer-fresh-gates') 89 const moderation = configureModerationForAccount(agent, account) 90 return agent.prepare(gates, moderation, onSessionChange) 91} 92 93export async function createAgentAndCreateAccount( 94 { 95 service, 96 email, 97 password, 98 handle, 99 birthDate, 100 inviteCode, 101 verificationPhone, 102 verificationCode, 103 }: { 104 service: string 105 email: string 106 password: string 107 handle: string 108 birthDate: Date 109 inviteCode?: string 110 verificationPhone?: string 111 verificationCode?: string 112 }, 113 onSessionChange: ( 114 agent: BskyAgent, 115 did: string, 116 event: AtpSessionEvent, 117 ) => void, 118) { 119 const agent = new BskyAppAgent({service}) 120 await agent.createAccount({ 121 email, 122 password, 123 handle, 124 inviteCode, 125 verificationPhone, 126 verificationCode, 127 }) 128 const account = agentToSessionAccountOrThrow(agent) 129 const gates = tryFetchGates(account.did, 'prefer-fresh-gates') 130 const moderation = configureModerationForAccount(agent, account) 131 132 // Not awaited so that we can still get into onboarding. 133 // This is OK because we won't let you toggle adult stuff until you set the date. 134 if (IS_PROD_SERVICE(service)) { 135 try { 136 networkRetry(1, async () => { 137 await agent.setPersonalDetails({birthDate: birthDate.toISOString()}) 138 await agent.overwriteSavedFeeds([ 139 { 140 ...DISCOVER_SAVED_FEED, 141 id: TID.nextStr(), 142 }, 143 { 144 ...TIMELINE_SAVED_FEED, 145 id: TID.nextStr(), 146 }, 147 ]) 148 149 if (getAge(birthDate) < 18) { 150 await agent.api.com.atproto.repo.putRecord({ 151 repo: account.did, 152 collection: 'chat.bsky.actor.declaration', 153 rkey: 'self', 154 record: { 155 $type: 'chat.bsky.actor.declaration', 156 allowIncoming: 'none', 157 }, 158 }) 159 } 160 }) 161 } catch (e: any) { 162 logger.error(e, { 163 context: `session: createAgentAndCreateAccount failed to save personal details and feeds`, 164 }) 165 } 166 } else { 167 agent.setPersonalDetails({birthDate: birthDate.toISOString()}) 168 } 169 170 try { 171 // snooze first prompt after signup, defer to next prompt 172 snoozeEmailConfirmationPrompt() 173 } catch (e: any) { 174 logger.error(e, {context: `session: failed snoozeEmailConfirmationPrompt`}) 175 } 176 177 return agent.prepare(gates, moderation, onSessionChange) 178} 179 180export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount { 181 const account = agentToSessionAccount(agent) 182 if (!account) { 183 throw Error('Expected an active session') 184 } 185 return account 186} 187 188export function agentToSessionAccount( 189 agent: BskyAgent, 190): SessionAccount | undefined { 191 if (!agent.session) { 192 return undefined 193 } 194 return { 195 service: agent.service.toString(), 196 did: agent.session.did, 197 handle: agent.session.handle, 198 email: agent.session.email, 199 emailConfirmed: agent.session.emailConfirmed || false, 200 emailAuthFactor: agent.session.emailAuthFactor || false, 201 refreshJwt: agent.session.refreshJwt, 202 accessJwt: agent.session.accessJwt, 203 signupQueued: isSignupQueued(agent.session.accessJwt), 204 active: agent.session.active, 205 status: agent.session.status as SessionAccount['status'], 206 pdsUrl: agent.pdsUrl?.toString(), 207 } 208} 209 210export function sessionAccountToSession( 211 account: SessionAccount, 212): AtpSessionData { 213 return { 214 // Sorted in the same property order as when returned by BskyAgent (alphabetical). 215 accessJwt: account.accessJwt ?? '', 216 did: account.did, 217 email: account.email, 218 emailAuthFactor: account.emailAuthFactor, 219 emailConfirmed: account.emailConfirmed, 220 handle: account.handle, 221 refreshJwt: account.refreshJwt ?? '', 222 /** 223 * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188 224 */ 225 active: account.active ?? true, 226 status: account.status, 227 } 228} 229 230// Not exported. Use factories above to create it. 231let realFetch = globalThis.fetch 232class BskyAppAgent extends BskyAgent { 233 persistSessionHandler: ((event: AtpSessionEvent) => void) | undefined = 234 undefined 235 236 constructor({service}: {service: string}) { 237 super({ 238 service, 239 async fetch(...args) { 240 let success = false 241 try { 242 const result = await realFetch(...args) 243 success = true 244 return result 245 } catch (e) { 246 success = false 247 throw e 248 } finally { 249 if (success) { 250 emitNetworkConfirmed() 251 } else { 252 emitNetworkLost() 253 } 254 } 255 }, 256 persistSession: (event: AtpSessionEvent) => { 257 if (this.persistSessionHandler) { 258 this.persistSessionHandler(event) 259 } 260 }, 261 }) 262 } 263 264 async prepare( 265 // Not awaited in the calling code so we can delay blocking on them. 266 gates: Promise<void>, 267 moderation: Promise<void>, 268 onSessionChange: ( 269 agent: BskyAgent, 270 did: string, 271 event: AtpSessionEvent, 272 ) => void, 273 ) { 274 // There's nothing else left to do, so block on them here. 275 await Promise.all([gates, moderation]) 276 277 // Now the agent is ready. 278 const account = agentToSessionAccountOrThrow(this) 279 let lastSession = this.sessionManager.session 280 this.persistSessionHandler = event => { 281 if (this.sessionManager.session) { 282 lastSession = this.sessionManager.session 283 } else if (event === 'network-error') { 284 // Put it back, we'll try again later. 285 this.sessionManager.session = lastSession 286 } 287 288 onSessionChange(this, account.did, event) 289 if (event !== 'create' && event !== 'update') { 290 addSessionErrorLog(account.did, event) 291 } 292 } 293 return {account, agent: this} 294 } 295 296 dispose() { 297 this.sessionManager.session = undefined 298 this.persistSessionHandler = undefined 299 } 300} 301 302export type {BskyAppAgent}