mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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