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-open 304 lines 8.7 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({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}