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 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}