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