mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}