diff --git a/patches/@atproto+api+0.18.18.patch b/patches/@atproto+api+0.18.18.patch new file mode 100644 index 000000000..aed3889a7 --- /dev/null +++ b/patches/@atproto+api+0.18.18.patch @@ -0,0 +1,30 @@ +diff --git a/node_modules/@atproto/api/dist/agent.js b/node_modules/@atproto/api/dist/agent.js +index 634e463..57b5c74 100644 +--- a/node_modules/@atproto/api/dist/agent.js ++++ b/node_modules/@atproto/api/dist/agent.js +@@ -593,7 +593,7 @@ class Agent extends xrpc_1.XrpcClient { + hideAllFeeds: false, + }, + }; +- const res = await this.app.bsky.actor.getPreferences({}); ++ const res = await this.app.bsky.actor.getPreferences({}, {headers: {'atproto-proxy': ''}}); + const labelPrefs = []; + for (const pref of res.data.preferences) { + if (predicate.isValidAdultContentPref(pref)) { +@@ -1275,14 +1275,14 @@ class Agent extends xrpc_1.XrpcClient { + async updatePreferences(cb) { + try { + await __classPrivateFieldGet(this, _Agent_prefsLock, "f").acquireAsync(); +- const res = await this.app.bsky.actor.getPreferences({}); ++ const res = await this.app.bsky.actor.getPreferences({}, {headers: {'atproto-proxy': ''}}); + const newPrefs = cb(res.data.preferences); + if (newPrefs === false) { + return res.data.preferences; + } + await this.app.bsky.actor.putPreferences({ + preferences: newPrefs, +- }); ++ }, {headers: {'atproto-proxy': ''}}); + return newPrefs; + } + finally { diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index 4d4e34536..93abfbd0a 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -17,6 +17,7 @@ import { type AppBskyFeedThreadgate, AtUri, type BlobRef, + isDid, type RichText as RichTextAPI, } from '@atproto/api' import {plural} from '@lingui/core/macro' @@ -609,8 +610,8 @@ let PostMenuItems = ({ if (!videoEmbed) return const did = post.author.did const cid = videoEmbed.cid - if (!did.startsWith('did:')) return - const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`) + if (!isDid(did)) return + const pdsUrl = await resolvePdsServiceUrl(did) const uri = `${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}` Toast.show(l({message: 'Downloading video...', context: 'toast'})) @@ -624,10 +625,9 @@ let PostMenuItems = ({ type: 'success', }) else - Toast.show( - l({message: 'Failed to download video', context: 'toast'}), - {type: 'error'}, - ) + Toast.show(l({message: 'Failed to download video', context: 'toast'}), { + type: 'error', + }) } const onPressDownloadGif = async () => { @@ -644,10 +644,9 @@ let PostMenuItems = ({ type: 'success', }) else - Toast.show( - l({message: 'Failed to download GIF', context: 'toast'}), - {type: 'error'}, - ) + Toast.show(l({message: 'Failed to download GIF', context: 'toast'}), { + type: 'error', + }) } const isEmbedGif = () => { diff --git a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts index 67466be92..ce68a9b8a 100644 --- a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts +++ b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts @@ -1,6 +1,7 @@ import {useMutation} from '@tanstack/react-query' import {useAgent, useSession} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' export function useConfirmEmail({ onSuccess, @@ -15,7 +16,7 @@ export function useConfirmEmail({ throw new Error('No email found for the current account') } - await agent.com.atproto.server.confirmEmail({ + await pdsAgent(agent).com.atproto.server.confirmEmail({ email: currentAccount.email.trim(), token: token.trim(), }) diff --git a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts index 358bf8654..b862d3a30 100644 --- a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts +++ b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts @@ -1,6 +1,7 @@ import {useMutation} from '@tanstack/react-query' import {useAgent, useSession} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' export function useManageEmail2FA() { const agent = useAgent() @@ -17,7 +18,7 @@ export function useManageEmail2FA() { throw new Error('No email found for the current account') } - await agent.com.atproto.server.updateEmail({ + await pdsAgent(agent).com.atproto.server.updateEmail({ email: currentAccount.email, emailAuthFactor: enabled, token, diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts index a442662fc..99816e663 100644 --- a/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts @@ -1,13 +1,15 @@ import {useMutation} from '@tanstack/react-query' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' export function useRequestEmailUpdate() { const agent = useAgent() return useMutation({ mutationFn: async () => { - return (await agent.com.atproto.server.requestEmailUpdate()).data + return (await pdsAgent(agent).com.atproto.server.requestEmailUpdate()) + .data }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts index ae308c7af..34cd5270f 100644 --- a/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts @@ -1,13 +1,14 @@ import {useMutation} from '@tanstack/react-query' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' export function useRequestEmailVerification() { const agent = useAgent() return useMutation({ mutationFn: async () => { - await agent.com.atproto.server.requestEmailConfirmation() + await pdsAgent(agent).com.atproto.server.requestEmailConfirmation() }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts b/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts index 2ec1eb6dc..644461f9f 100644 --- a/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts +++ b/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts @@ -1,6 +1,7 @@ import {useMutation} from '@tanstack/react-query' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate' async function updateEmailAndRefreshSession( @@ -8,7 +9,10 @@ async function updateEmailAndRefreshSession( email: string, token?: string, ) { - await agent.com.atproto.server.updateEmail({email: email.trim(), token}) + await pdsAgent(agent).com.atproto.server.updateEmail({ + email: email.trim(), + token, + }) await agent.resumeSession(agent.session!) } diff --git a/src/components/intents/VerifyEmailIntentDialog.tsx b/src/components/intents/VerifyEmailIntentDialog.tsx index b3504766f..ed647980e 100644 --- a/src/components/intents/VerifyEmailIntentDialog.tsx +++ b/src/components/intents/VerifyEmailIntentDialog.tsx @@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useAgent, useSession} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -52,7 +53,7 @@ function Inner({}: {control: DialogControlProps}) { const onPressResendEmail = async () => { setSending(true) - await agent.com.atproto.server.requestEmailConfirmation() + await pdsAgent(agent).com.atproto.server.requestEmailConfirmation() setSending(false) setStatus('resent') } diff --git a/src/env/common.ts b/src/env/common.ts index 606635958..c311a32b6 100644 --- a/src/env/common.ts +++ b/src/env/common.ts @@ -141,3 +141,9 @@ export const APP_CONFIG_PROD_URL = `https://app-config.workers.bsky.app` export const APP_CONFIG_URL = IS_DEV ? (APP_CONFIG_DEV_URL ?? APP_CONFIG_PROD_URL) : APP_CONFIG_PROD_URL + +export const ENV_PUBLIC_BSKY_SERVICE: string | undefined = + process.env.EXPO_PUBLIC_PUBLIC_BSKY_SERVICE +export const ENV_APPVIEW_DID_PROXY: + | `did:${string}:${string}#bsky_appview` + | undefined = process.env.EXPO_PUBLIC_APPVIEW_DID_PROXY diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 18bb8c8f0..d6bea2c79 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -5,6 +5,7 @@ import { jsonStringToLex, } from '@atproto/api' +import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' import { getAppLanguageAsContentLanguage, getContentLanguages, @@ -120,7 +121,7 @@ async function loggedOutFetch({ // manually construct fetch call so we can add the `lang` cache-busting param let res = await fetch( - `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ + `${PUBLIC_BSKY_SERVICE}/xrpc/app.bsky.feed.getFeed?feed=${feed}${ cursor ? `&cursor=${cursor}` : '' }&limit=${limit}&lang=${contentLangs}`, { @@ -140,7 +141,7 @@ async function loggedOutFetch({ // no data, try again with language headers removed res = await fetch( - `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ + `${PUBLIC_BSKY_SERVICE}/xrpc/app.bsky.feed.getFeed?feed=${feed}${ cursor ? `&cursor=${cursor}` : '' }&limit=${limit}`, {method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}}, diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 2732d1f26..3a50f3945 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -38,6 +38,7 @@ import { createThreadgateRecord, threadgateAllowUISettingToAllowRecordValue, } from '#/state/queries/threadgate' +import {pdsAgent} from '#/state/session/agent' import { type EmbedDraft, type PostDraft, @@ -182,7 +183,7 @@ export async function post( } try { - await agent.com.atproto.repo.applyWrites({ + await pdsAgent(agent).com.atproto.repo.applyWrites({ repo: agent.assertDid, writes: writes, validate: true, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a52ac8795..8b0ac8428 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -2,19 +2,25 @@ import {type Insets, Platform} from 'react-native' import {type AppBskyActorDefs, BSKY_LABELER_DID} from '@atproto/api' import {type ProxyHeaderValue} from '#/state/session/agent' -import {BLUESKY_PROXY_DID, CHAT_PROXY_DID} from '#/env' - +import { + BLUESKY_PROXY_DID, + CHAT_PROXY_DID, + ENV_APPVIEW_DID_PROXY, + ENV_PUBLIC_BSKY_SERVICE, +} from '#/env' export const LOCAL_DEV_SERVICE = Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583' export const STAGING_SERVICE = 'https://staging.bsky.dev' export const BSKY_SERVICE = 'https://bsky.social' export const BSKY_SERVICE_DID = 'did:web:bsky.social' -export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' +export const PUBLIC_BSKY_SERVICE = + ENV_PUBLIC_BSKY_SERVICE || 'https://public.api.bsky.app' export const DEFAULT_SERVICE = BSKY_SERVICE export const HELP_DESK_URL = `https://tangled.org/jollywhoppers.com/witchsky.app/` export const EMBED_SERVICE = 'https://embed.bsky.app' export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download' +export const APPVIEW_DID_PROXY = ENV_APPVIEW_DID_PROXY export const STARTER_PACK_MAX_SIZE = 150 export const CARD_ASPECT_RATIO = 1200 / 630 diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts index 1f4265a17..9f34ccbf5 100644 --- a/src/lib/generate-starterpack.ts +++ b/src/lib/generate-starterpack.ts @@ -15,6 +15,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {enforceLen} from '#/lib/strings/helpers' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import type * as bsky from '#/types/bsky' export const createStarterPackList = async ({ @@ -44,7 +45,7 @@ export const createStarterPackList = async ({ }, ) if (!list) throw new Error('List creation failed') - await agent.com.atproto.repo.applyWrites({ + await pdsAgent(agent).com.atproto.repo.applyWrites({ repo: agent.session!.did, writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})), }) diff --git a/src/lib/media/video/upload.shared.ts b/src/lib/media/video/upload.shared.ts index 1ab8439e6..d21ea5825 100644 --- a/src/lib/media/video/upload.shared.ts +++ b/src/lib/media/video/upload.shared.ts @@ -5,6 +5,7 @@ import {msg} from '@lingui/core/macro' import {VIDEO_SERVICE_DID} from '#/lib/constants' import {UploadLimitError} from '#/lib/media/video/errors' import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers' +import {pdsAgent} from '#/state/session/agent' import {createVideoAgent} from './util' export async function getServiceAuthToken({ @@ -22,7 +23,9 @@ export async function getServiceAuthToken({ if (!pdsAud) { throw new Error('Agent does not have a PDS URL') } - const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth({ + const {data: serviceAuth} = await pdsAgent( + agent, + ).com.atproto.server.getServiceAuth({ aud: aud ?? pdsAud, lxm, exp, diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index ec657b6c4..a943b502a 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -13,6 +13,7 @@ import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' import {PERSISTED_QUERY_ROOT} from '#/state/queries' import * as env from '#/env' import {IS_NATIVE, IS_WEB} from '#/env' +import {PUBLIC_BSKY_SERVICE} from './constants' declare global { interface Window { @@ -27,7 +28,7 @@ async function checkIsOnline(): Promise { setTimeout(() => { controller.abort() }, 15e3) - const res = await fetch('https://public.api.bsky.app/xrpc/_health', { + const res = await fetch(`${PUBLIC_BSKY_SERVICE}/xrpc/_health`, { cache: 'no-store', signal: controller.signal, }) diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx index 24d6cd90a..6c3f61ee0 100644 --- a/src/screens/Deactivated.tsx +++ b/src/screens/Deactivated.tsx @@ -14,6 +14,7 @@ import { useSession, useSessionApi, } from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {Logo} from '#/view/icons/Logo' import {atoms as a, useTheme} from '#/alf' @@ -70,7 +71,7 @@ export function Deactivated() { const handleActivate = useCallback(async () => { try { setPending(true) - await agent.com.atproto.server.activateAccount() + await pdsAgent(agent).com.atproto.server.activateAccount() await queryClient.resetQueries() await agent.resumeSession(agent.session!) } catch (e: any) { diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts index acb96ee91..7e0a167c0 100644 --- a/src/screens/Onboarding/util.ts +++ b/src/screens/Onboarding/util.ts @@ -10,6 +10,7 @@ import {TID} from '@atproto/common-web' import chunk from 'lodash.chunk' import {until} from '#/lib/async/until' +import {pdsAgent} from '#/state/session/agent' export async function bulkWriteFollows( agent: BskyAgent, @@ -41,7 +42,7 @@ export async function bulkWriteFollows( const chunks = chunk(followWrites, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ + await pdsAgent(agent).com.atproto.repo.applyWrites({ repo: session.did, writes: chunk, }) diff --git a/src/screens/Settings/RunesSettings.tsx b/src/screens/Settings/RunesSettings.tsx index f9550d5ed..2e1eb0364 100644 --- a/src/screens/Settings/RunesSettings.tsx +++ b/src/screens/Settings/RunesSettings.tsx @@ -1,5 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' +import {isDid} from '@atproto/api' import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -18,6 +19,10 @@ import { useConstellationInstance, useSetConstellationInstance, } from '#/state/preferences/constellation-instance' +import { + useCustomAppViewDid, + useSetCustomAppViewDid, +} from '#/state/preferences/custom-appview-did' import { useDeerVerificationEnabled, useDeerVerificationTrusted, @@ -152,6 +157,8 @@ import { useSetHandleInLinks, } from '#/state/preferences/use-handle-in-links' import {useProfilesQuery} from '#/state/queries/profile' +import {findService, useDidDocument} from '#/state/queries/resolve-identity' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useBreakpoints} from '#/alf' import {Admonition} from '#/components/Admonition' @@ -256,6 +263,140 @@ function ConstellationInstanceDialog({ ) } +function CustomAppViewDidDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {_} = useLingui() + + const [customAppViewDid] = useCustomAppViewDid() + const [did, setDid] = useState(customAppViewDid ?? '') + const setCustomAppViewDid = useSetCustomAppViewDid() + + const doc = useDidDocument({did}) + const bskyAppViewService = + doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView') + + const submit = () => { + if (did.length === 0) { + control.close(() => { + setCustomAppViewDid(undefined) + }) + return + } + if (!bskyAppViewService?.serviceEndpoint) return + control.close(() => { + setCustomAppViewDid(did) + }) + } + + return ( + setDid(customAppViewDid ?? '')}> + + + + + Custom AppView Proxy DID + + + + + { + setDid(value) + }} + placeholder={ + APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) || + `did:web:api.bsky.app` + } + placeholderTextColor={pal.colors.textLight} + onSubmitEditing={submit} + accessibilityHint={_( + msg`Input the DID of the AppView to proxy requests through`, + )} + isInvalid={ + !!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading + } + defaultValue={customAppViewDid ?? ''} + /> + + {did && !isDid(did) && ( + + + + )} + + {did && (did.includes('#') || did.includes('?')) && ( + + + + )} + + {doc.isError && ( + + + + )} + + {doc.data && + !bskyAppViewService && + (doc.data as {message?: string}).message && ( + + + + )} + + {doc.data && !bskyAppViewService && ( + + + + )} + + {bskyAppViewService && ( + + {JSON.stringify(bskyAppViewService, null, 2)} + + )} + + + + + + + + + + ) +} + function LibreTranslateInstanceDialog({ control, }: { @@ -836,6 +977,9 @@ export function RunesSettingsScreen({}: Props) { const autoLikeOnRepost = useAutoLikeOnRepost() const setAutoLikeOnRepost = useSetAutoLikeOnRepost() + const [customAppViewDid] = useCustomAppViewDid() + const setCustomAppViewDidControl = Dialog.useDialogControl() + return ( @@ -1010,8 +1154,6 @@ export function RunesSettingsScreen({}: Props) { /> - - @@ -1499,6 +1641,19 @@ export function RunesSettingsScreen({}: Props) { + + + + {`Custom AppView DID`} + + setCustomAppViewDidControl.open()} + /> + + + + @@ -1543,6 +1698,7 @@ export function RunesSettingsScreen({}: Props) { + { try { setPending(true) - await agent.com.atproto.server.deactivateAccount({}) + await pdsAgent(agent).com.atproto.server.deactivateAccount({}) control.close(() => { logoutCurrentAccount('Deactivated') }) diff --git a/src/screens/Settings/components/DisableEmail2FADialog.tsx b/src/screens/Settings/components/DisableEmail2FADialog.tsx index 8b774b54f..29dbd580c 100644 --- a/src/screens/Settings/components/DisableEmail2FADialog.tsx +++ b/src/screens/Settings/components/DisableEmail2FADialog.tsx @@ -6,6 +6,7 @@ import {Trans} from '@lingui/react/macro' import {cleanError} from '#/lib/strings/errors' import {useAgent, useSession} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -42,7 +43,7 @@ export function DisableEmail2FADialog({ setError('') setIsProcessing(true) try { - await agent.com.atproto.server.requestEmailUpdate() + await pdsAgent(agent).com.atproto.server.requestEmailUpdate() setStage(Stages.ConfirmCode) } catch (e) { setError(cleanError(String(e))) @@ -56,7 +57,7 @@ export function DisableEmail2FADialog({ setIsProcessing(true) try { if (currentAccount?.email) { - await agent.com.atproto.server.updateEmail({ + await pdsAgent(agent).com.atproto.server.updateEmail({ email: currentAccount.email, token: confirmationCode.trim(), emailAuthFactor: false, diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx index df064e3a0..641391b14 100644 --- a/src/screens/Settings/components/ExportCarDialog.tsx +++ b/src/screens/Settings/components/ExportCarDialog.tsx @@ -8,6 +8,7 @@ import {DM_SERVICE_HEADERS} from '#/lib/constants' import {saveBytesToDisk} from '#/lib/media/manip' import {logger} from '#/logger' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -34,7 +35,7 @@ export function ExportCarDialog({ try { setLoading('repo') const did = agent.session.did - const downloadRes = await agent.com.atproto.sync.getRepo({did}) + const downloadRes = await pdsAgent(agent).com.atproto.sync.getRepo({did}) const saveRes = await saveBytesToDisk( 'repo.car', downloadRes.data, diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx index 406b785d2..58aa86e16 100644 --- a/src/screens/SignupQueued.tsx +++ b/src/screens/SignupQueued.tsx @@ -8,6 +8,7 @@ import {Trans} from '@lingui/react/macro' import {logger} from '#/logger' import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {useOnboardingDispatch} from '#/state/shell' import {Logo} from '#/view/icons/Logo' import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' @@ -38,7 +39,7 @@ export function SignupQueued() { const checkStatus = useCallback(async () => { setProcessing(true) try { - const res = await agent.com.atproto.temp.checkSignupQueue() + const res = await pdsAgent(agent).com.atproto.temp.checkSignupQueue() if (res.data.activated) { // ready to go, exchange the access token for a usable one and kick off onboarding await agent.sessionManager.refreshSession() diff --git a/src/state/preferences/custom-appview-did.tsx b/src/state/preferences/custom-appview-did.tsx new file mode 100644 index 000000000..470941658 --- /dev/null +++ b/src/state/preferences/custom-appview-did.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import {reloadAppAsync} from 'expo' +import {isDid} from '@atproto/api' + +import {IS_WEB} from '#/env' +import {device, useStorage} from '#/storage' + +export function useCustomAppViewDid() { + const [customAppViewDid = undefined, setCustomAppViewDid] = useStorage( + device, + ['customAppViewDid'], + ) + + return [customAppViewDid, setCustomAppViewDid] as const +} + +export function useSetCustomAppViewDid() { + const [, setCustomAppViewDid] = useCustomAppViewDid() + + return React.useCallback( + (customAppViewDid: string | undefined) => { + setCustomAppViewDid(customAppViewDid) + + if (IS_WEB) { + window.location.reload() + } else { + void reloadAppAsync() + } + }, + [setCustomAppViewDid], + ) +} + +export function readCustomAppViewDidUri() { + const maybeDid = device.get(['customAppViewDid']) + if (!maybeDid || !isDid(maybeDid)) { + return undefined + } + + return `${maybeDid}#bsky_appview` +} diff --git a/src/state/queries/app-passwords.ts b/src/state/queries/app-passwords.ts index 12d66dd2c..ace23ab00 100644 --- a/src/state/queries/app-passwords.ts +++ b/src/state/queries/app-passwords.ts @@ -3,6 +3,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {STALE} from '#/state/queries' import {useAgent} from '../session' +import {pdsAgent} from '../session/agent' const RQKEY_ROOT = 'app-passwords' export const RQKEY = () => [RQKEY_ROOT] @@ -13,7 +14,7 @@ export function useAppPasswordsQuery() { staleTime: STALE.MINUTES.FIVE, queryKey: RQKEY(), queryFn: async () => { - const res = await agent.com.atproto.server.listAppPasswords({}) + const res = await pdsAgent(agent).com.atproto.server.listAppPasswords({}) return res.data.passwords }, }) @@ -29,7 +30,7 @@ export function useAppPasswordCreateMutation() { >({ mutationFn: async ({name, privileged}) => { return ( - await agent.com.atproto.server.createAppPassword({ + await pdsAgent(agent).com.atproto.server.createAppPassword({ name, privileged, }) @@ -48,7 +49,7 @@ export function useAppPasswordDeleteMutation() { const agent = useAgent() return useMutation({ mutationFn: async ({name}) => { - await agent.com.atproto.server.revokeAppPassword({ + await pdsAgent(agent).com.atproto.server.revokeAppPassword({ name, }) }, diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index c0d5edfb1..73e454819 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -17,6 +17,7 @@ import {until} from '#/lib/async/until' import {type ImageMeta} from '#/state/gallery' import {STALE} from '#/state/queries' import {useAgent, useSession} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {invalidate as invalidateMyLists} from './my-lists' import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists' @@ -152,7 +153,7 @@ export function useListMetadataMutation() { record.avatar = undefined } const res = ( - await agent.com.atproto.repo.putRecord({ + await pdsAgent(agent).com.atproto.repo.putRecord({ repo: currentAccount.did, collection: 'app.bsky.graph.list', rkey, @@ -231,7 +232,7 @@ export function useListDeleteMutation() { // apply in chunks for (const writesChunk of chunk(writes, 10)) { - await agent.com.atproto.repo.applyWrites({ + await pdsAgent(agent).com.atproto.repo.applyWrites({ repo: currentAccount.did, writes: writesChunk, }) diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index a5adb39d9..4210cab3b 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -3,6 +3,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {useAgent, useSession} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {RQKEY as PROFILE_RKEY} from '../profile' export function useUpdateActorDeclaration({ @@ -19,7 +20,7 @@ export function useUpdateActorDeclaration({ return useMutation({ mutationFn: async (allowIncoming: 'all' | 'none' | 'following') => { if (!currentAccount) throw new Error('Not signed in') - const result = await agent.com.atproto.repo.putRecord({ + const result = await pdsAgent(agent).com.atproto.repo.putRecord({ repo: currentAccount.did, collection: 'chat.bsky.actor.declaration', rkey: 'self', @@ -69,7 +70,7 @@ export function useDeleteActorDeclaration() { return useMutation({ mutationFn: async () => { if (!currentAccount) throw new Error('Not signed in') - const result = await agent.api.com.atproto.repo.deleteRecord({ + const result = await pdsAgent(agent).com.atproto.repo.deleteRecord({ repo: currentAccount.did, collection: 'chat.bsky.actor.declaration', rkey: 'self', diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index 926bb0ba0..793c6bfb7 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -21,6 +21,7 @@ import { POSTGATE_COLLECTION, } from '#/state/queries/postgate/util' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import * as bsky from '#/types/bsky' export async function getPostgateRecord({ @@ -96,7 +97,7 @@ export async function writePostgateRecord({ const postUrip = new AtUri(postUri) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ + pdsAgent(agent).com.atproto.repo.putRecord({ repo: agent.session!.did, collection: POSTGATE_COLLECTION, rkey: postUrip.rkey, diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index e78a5d73f..0aa32a693 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -23,7 +23,8 @@ import { type ThreadViewPreferences, type UsePreferencesQueryResponse, } from '#/state/queries/preferences/types' -import {useAgent} from '#/state/session' +import {useBlankPrefAuthedAgent as useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {saveLabelers} from '#/state/session/agent-config' import {useAgeAssurance} from '#/ageAssurance' import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util' @@ -49,7 +50,7 @@ export function usePreferencesQuery() { if (!agent.did) { return DEFAULT_LOGGED_OUT_PREFERENCES } else { - const res = await agent.getPreferences() + const res = await pdsAgent(agent).getPreferences() // save to local storage to ensure there are labels on initial requests saveLabelers( @@ -113,7 +114,7 @@ export function useClearPreferencesMutation() { return useMutation({ mutationFn: async () => { - await agent.app.bsky.actor.putPreferences({preferences: []}) + await pdsAgent(agent).app.bsky.actor.putPreferences({preferences: []}) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, diff --git a/src/state/queries/resolve-identity.ts b/src/state/queries/resolve-identity.ts index 74fb92404..f2409e390 100644 --- a/src/state/queries/resolve-identity.ts +++ b/src/state/queries/resolve-identity.ts @@ -1,26 +1,68 @@ +import {type Did, isDid} from '@atproto/api' +import {useQuery} from '@tanstack/react-query' + +import {STALE} from '.' import {LRU} from './direct-fetch-record' +const RQKEY_ROOT = 'resolve-identity' +export const RQKEY = (did: string) => [RQKEY_ROOT, did] + +// this isn't trusted... +export type DidDocument = { + '@context'?: string[] + id?: string + alsoKnownAs?: string[] + verificationMethod?: VerificationMethod[] + service?: Service[] +} + +export type VerificationMethod = { + id?: string + type?: string + controller?: string + publicKeyMultibase?: string +} + +export type Service = { + id?: string + type?: string + serviceEndpoint?: string +} -const serviceCache = new LRU<`did:${string}`, string>() +const serviceCache = new LRU() -export async function resolvePdsServiceUrl(did: `did:${string}`) { +export async function resolveDidDocument(did: Did) { return await serviceCache.getOrTryInsertWith(did, async () => { const docUrl = did.startsWith('did:plc:') ? `https://plc.directory/${did}` : `https://${did.substring(8)}/.well-known/did.json` - // TODO: validate! - const doc: { - service: { - serviceEndpoint: string - type: string - }[] - } = await (await fetch(docUrl)).json() - const service = doc.service.find( - s => s.type === 'AtprotoPersonalDataServer', - )?.serviceEndpoint - - if (service === undefined) - throw new Error(`could not find a service for ${did}`) - return service + // TODO: we should probably validate this... + return await (await fetch(docUrl)).json() + }) +} + +export function findService(doc: DidDocument, id: string, type?: string) { + // probably not defensive enough, but we don't have atproto/did as a dep... + if (!Array.isArray(doc?.service)) return + return doc.service.find( + s => s?.serviceEndpoint && s?.id === id && (!type || s?.type === type), + ) +} + +export async function resolvePdsServiceUrl(did: Did) { + const doc = await resolveDidDocument(did) + return findService(doc, '#atproto_pds', 'AtprotoPersonalDataServer') + ?.serviceEndpoint +} + +export function useDidDocument({did}: {did: string}) { + return useQuery({ + staleTime: STALE.HOURS.ONE, + queryKey: RQKEY(did || ''), + async queryFn() { + if (!isDid(did)) return undefined + return await resolveDidDocument(did) + }, + enabled: isDid(did) && !(did.includes('#') || did.includes('?')), }) } diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index 53d668d89..65c9b3c34 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -27,6 +27,7 @@ import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-pa import {STALE} from '#/state/queries/index' import {invalidateListMembersQuery} from '#/state/queries/list-members' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import * as bsky from '#/types/bsky' const RQKEY_ROOT = 'starter-pack' @@ -203,7 +204,7 @@ export function useEditStarterPackMutation({ if (removedItems.length !== 0) { const chunks = chunk(removedItems, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ + await pdsAgent(agent).com.atproto.repo.applyWrites({ repo: agent.session!.did, writes: chunk.map(i => ({ $type: 'com.atproto.repo.applyWrites#delete', @@ -220,7 +221,7 @@ export function useEditStarterPackMutation({ if (addedProfiles.length > 0) { const chunks = chunk(addedProfiles, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ + await pdsAgent(agent).com.atproto.repo.applyWrites({ repo: agent.session!.did, writes: chunk.map(p => ({ $type: 'com.atproto.repo.applyWrites#create', @@ -237,7 +238,7 @@ export function useEditStarterPackMutation({ } const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey - await agent.com.atproto.repo.putRecord({ + await pdsAgent(agent).com.atproto.repo.putRecord({ repo: agent.session!.did, collection: 'app.bsky.graph.starterpack', rkey, diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index e760873fb..d5929c285 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -18,6 +18,7 @@ import { } from '#/state/queries/threadgate/util' import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' import {useAgent} from '#/state/session' +import {pdsAgent} from '#/state/session/agent' import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' import * as bsky from '#/types/bsky' @@ -162,7 +163,7 @@ export async function writeThreadgateRecord({ }) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ + pdsAgent(agent).com.atproto.repo.putRecord({ repo: agent.session!.did, collection: 'app.bsky.feed.threadgate', rkey: postUrip.rkey, diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 4065a9a03..ea9be1084 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -15,6 +15,7 @@ import {type FetchHandlerOptions} from '@atproto/xrpc' import {networkRetry} from '#/lib/async/retry' import { + APPVIEW_DID_PROXY, BLUESKY_PROXY_HEADER, BSKY_SERVICE, DISCOVER_SAVED_FEED, @@ -33,6 +34,7 @@ import { } from '#/ageAssurance/data' import {features} from '#/analytics' import {emitNetworkConfirmed, emitNetworkLost} from '../events' +import {readCustomAppViewDidUri} from '../preferences/custom-appview-did' import {addSessionErrorLog} from './logging' import { configureModerationForAccount, @@ -47,7 +49,9 @@ export function createPublicAgent() { configureModerationForGuest() // Side effect but only relevant for tests const agent = new BskyAppAgent({service: PUBLIC_BSKY_SERVICE}) - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + const proxyDid = + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY + agent.configureProxy(proxyDid) return agent } @@ -77,7 +81,9 @@ export async function createAgentAndResume( // after session is attached const aa = prefetchAgeAssuranceData({agent}) - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + const proxyDid = + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY + agent.configureProxy(proxyDid) return agent.prepare({ resolvers: [gates, moderation, aa], @@ -116,7 +122,9 @@ export async function createAgentAndLogin( const moderation = configureModerationForAccount(agent, account) const aa = prefetchAgeAssuranceData({agent}) - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + const proxyDid = + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY + agent.configureProxy(proxyDid) return agent.prepare({ resolvers: [gates, moderation, aa], @@ -223,7 +231,7 @@ export async function createAgentAndCreateAccount( }), getAge(birthDate) < 18 && networkRetry(3, () => { - return agent.com.atproto.repo.putRecord({ + return pdsAgent(agent).com.atproto.repo.putRecord({ repo: account.did, collection: 'chat.bsky.actor.declaration', rkey: 'self', @@ -288,7 +296,9 @@ export async function createAgentAndCreateAccount( logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`}) } - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + const proxyDid = + readCustomAppViewDidUri() || BLUESKY_PROXY_HEADER.get() || APPVIEW_DID_PROXY + agent.configureProxy(proxyDid) return agent.prepare({ resolvers: [gates, moderation, aa], @@ -400,6 +410,10 @@ class BskyAppAgent extends BskyAgent { } }, }) + const proxyDid = readCustomAppViewDidUri() || APPVIEW_DID_PROXY + if (proxyDid) { + this.configureProxy(proxyDid) + } } async prepare({ @@ -432,6 +446,12 @@ class BskyAppAgent extends BskyAgent { this.sessionManager.session = undefined this.persistSessionHandler = undefined } + + cloneWithoutProxy(): BskyAgent { + const cloned = new BskyAgent({service: this.serviceUrl.toString()}) + cloned.sessionManager.session = this.sessionManager.session + return cloned + } } /** diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index acdd8a4c9..fa0ee6a9b 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -22,6 +22,7 @@ import { createAgentAndCreateAccount, createAgentAndLogin, createAgentAndResume, + pdsAgent, sessionAccountToSession, } from './agent' import {type Action, getInitialState, reducer, type State} from './reducer' @@ -278,7 +279,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { >(async () => { const agent = state.currentAgentState.agent as BskyAppAgent const signal = cancelPendingTask() - const {data} = await agent.com.atproto.server.getSession() + const {data} = await pdsAgent(agent).com.atproto.server.getSession() if (signal.aborted) return store.dispatch({ type: 'partial-refresh-session', @@ -455,3 +456,14 @@ export function useAgent(): BskyAgent { } return agent } + +export function useBlankPrefAuthedAgent(): BskyAgent { + const agent = useContext(AgentContext) + if (!agent) { + throw Error('useAgent() must be below .') + } + + return useMemo(() => { + return (agent as BskyAppAgent).cloneWithoutProxy() + }, [agent]) +} diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 4d2089c9c..6ad122c6c 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -63,6 +63,7 @@ export type Device = { deerGateCache: string activitySubscriptionsNudged?: boolean threadgateNudged?: boolean + customAppViewDid: string | undefined /** * Policy update overlays. New IDs are required for each new announcement.