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({identifier, password, authFactorToken})
87
88 const account = agentToSessionAccountOrThrow(agent)
89 const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
90 const moderation = configureModerationForAccount(agent, account)
91 return agent.prepare(gates, moderation, onSessionChange)
92}
93
94export async function createAgentAndCreateAccount(
95 {
96 service,
97 email,
98 password,
99 handle,
100 birthDate,
101 inviteCode,
102 verificationPhone,
103 verificationCode,
104 }: {
105 service: string
106 email: string
107 password: string
108 handle: string
109 birthDate: Date
110 inviteCode?: string
111 verificationPhone?: string
112 verificationCode?: string
113 },
114 onSessionChange: (
115 agent: BskyAgent,
116 did: string,
117 event: AtpSessionEvent,
118 ) => void,
119) {
120 const agent = new BskyAppAgent({service})
121 await agent.createAccount({
122 email,
123 password,
124 handle,
125 inviteCode,
126 verificationPhone,
127 verificationCode,
128 })
129 const account = agentToSessionAccountOrThrow(agent)
130 const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
131 const moderation = configureModerationForAccount(agent, account)
132
133 // Not awaited so that we can still get into onboarding.
134 // This is OK because we won't let you toggle adult stuff until you set the date.
135 if (IS_PROD_SERVICE(service)) {
136 try {
137 networkRetry(1, async () => {
138 await agent.setPersonalDetails({birthDate: birthDate.toISOString()})
139 await agent.overwriteSavedFeeds([
140 {
141 ...DISCOVER_SAVED_FEED,
142 id: TID.nextStr(),
143 },
144 {
145 ...TIMELINE_SAVED_FEED,
146 id: TID.nextStr(),
147 },
148 ])
149
150 if (getAge(birthDate) < 18) {
151 await agent.api.com.atproto.repo.putRecord({
152 repo: account.did,
153 collection: 'chat.bsky.actor.declaration',
154 rkey: 'self',
155 record: {
156 $type: 'chat.bsky.actor.declaration',
157 allowIncoming: 'none',
158 },
159 })
160 }
161 })
162 } catch (e: any) {
163 logger.error(e, {
164 context: `session: createAgentAndCreateAccount failed to save personal details and feeds`,
165 })
166 }
167 } else {
168 agent.setPersonalDetails({birthDate: birthDate.toISOString()})
169 }
170
171 try {
172 // snooze first prompt after signup, defer to next prompt
173 snoozeEmailConfirmationPrompt()
174 } catch (e: any) {
175 logger.error(e, {context: `session: failed snoozeEmailConfirmationPrompt`})
176 }
177
178 return agent.prepare(gates, moderation, onSessionChange)
179}
180
181export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount {
182 const account = agentToSessionAccount(agent)
183 if (!account) {
184 throw Error('Expected an active session')
185 }
186 return account
187}
188
189export function agentToSessionAccount(
190 agent: BskyAgent,
191): SessionAccount | undefined {
192 if (!agent.session) {
193 return undefined
194 }
195 return {
196 service: agent.service.toString(),
197 did: agent.session.did,
198 handle: agent.session.handle,
199 email: agent.session.email,
200 emailConfirmed: agent.session.emailConfirmed || false,
201 emailAuthFactor: agent.session.emailAuthFactor || false,
202 refreshJwt: agent.session.refreshJwt,
203 accessJwt: agent.session.accessJwt,
204 signupQueued: isSignupQueued(agent.session.accessJwt),
205 active: agent.session.active,
206 status: agent.session.status as SessionAccount['status'],
207 pdsUrl: agent.pdsUrl?.toString(),
208 isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE),
209 }
210}
211
212export function sessionAccountToSession(
213 account: SessionAccount,
214): AtpSessionData {
215 return {
216 // Sorted in the same property order as when returned by BskyAgent (alphabetical).
217 accessJwt: account.accessJwt ?? '',
218 did: account.did,
219 email: account.email,
220 emailAuthFactor: account.emailAuthFactor,
221 emailConfirmed: account.emailConfirmed,
222 handle: account.handle,
223 refreshJwt: account.refreshJwt ?? '',
224 /**
225 * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188
226 */
227 active: account.active ?? true,
228 status: account.status,
229 }
230}
231
232// Not exported. Use factories above to create it.
233let realFetch = globalThis.fetch
234class BskyAppAgent extends BskyAgent {
235 persistSessionHandler: ((event: AtpSessionEvent) => void) | undefined =
236 undefined
237
238 constructor({service}: {service: string}) {
239 super({
240 service,
241 async fetch(...args) {
242 let success = false
243 try {
244 const result = await realFetch(...args)
245 success = true
246 return result
247 } catch (e) {
248 success = false
249 throw e
250 } finally {
251 if (success) {
252 emitNetworkConfirmed()
253 } else {
254 emitNetworkLost()
255 }
256 }
257 },
258 persistSession: (event: AtpSessionEvent) => {
259 if (this.persistSessionHandler) {
260 this.persistSessionHandler(event)
261 }
262 },
263 })
264 }
265
266 async prepare(
267 // Not awaited in the calling code so we can delay blocking on them.
268 gates: Promise<void>,
269 moderation: Promise<void>,
270 onSessionChange: (
271 agent: BskyAgent,
272 did: string,
273 event: AtpSessionEvent,
274 ) => void,
275 ) {
276 // There's nothing else left to do, so block on them here.
277 await Promise.all([gates, moderation])
278
279 // Now the agent is ready.
280 const account = agentToSessionAccountOrThrow(this)
281 let lastSession = this.sessionManager.session
282 this.persistSessionHandler = event => {
283 if (this.sessionManager.session) {
284 lastSession = this.sessionManager.session
285 } else if (event === 'network-error') {
286 // Put it back, we'll try again later.
287 this.sessionManager.session = lastSession
288 }
289
290 onSessionChange(this, account.did, event)
291 if (event !== 'create' && event !== 'update') {
292 addSessionErrorLog(account.did, event)
293 }
294 }
295 return {account, agent: this}
296 }
297
298 dispose() {
299 this.sessionManager.session = undefined
300 this.persistSessionHandler = undefined
301 }
302}
303
304export type {BskyAppAgent}