Mirror — see github.com/blacksky-algorithms/blacksky.community
6
fork

Configure Feed

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

Switch appview from api.bsky.app to api.blacksky.community

- Default proxy DID: did:web:api.blacksky.community
- Public appview: https://api.blacksky.community
- Video service: https://video.blacksky.community (60 min / 5GB limits)
- Route trending feed requests through Bluesky appview via ALT_PROXY_DID
- Update embeds, functions, and test snapshots

+174 -28
+1 -1
bskyembed/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Bluesky Embed</title> 7 - <link rel="preconnect" href="https://api.bsky.app"> 7 + <link rel="preconnect" href="https://api.blacksky.community"> 8 8 <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"> 9 9 <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> 10 10 <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
+1 -1
bskyembed/post.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Blacksky Embed</title> 7 - <link rel="preconnect" href="https://api.bsky.app"> 7 + <link rel="preconnect" href="https://api.blacksky.community"> 8 8 <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"> 9 9 <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> 10 10 <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
+1 -1
bskyembed/src/screens/landing.tsx
··· 31 31 initSystemColorMode() 32 32 33 33 const agent = new AtpAgent({ 34 - service: 'https://api.bsky.app', 34 + service: 'https://api.blacksky.community', 35 35 }) 36 36 37 37 render(<LandingPage />, root)
+1 -1
bskyembed/src/screens/post.tsx
··· 14 14 if (!root) throw new Error('No root element') 15 15 16 16 const agent = new AtpAgent({ 17 - service: 'https://api.bsky.app', 17 + service: 'https://api.blacksky.community', 18 18 }) 19 19 20 20 const uri = `at://${window.location.pathname.slice('/embed/'.length)}`
+1 -1
bskyweb/cmd/embedr/main.go
··· 36 36 &cli.StringFlag{ 37 37 Name: "appview-host", 38 38 Usage: "method, hostname, and port of PDS instance", 39 - Value: "https://api.bsky.app", 39 + Value: "https://api.blacksky.community", 40 40 EnvVars: []string{"ATP_APPVIEW_HOST"}, 41 41 }, 42 42 &cli.StringFlag{
+1 -1
bskyweb/example.env
··· 1 1 GOLOG_LOG_LEVEL=info 2 - ATP_APPVIEW_HOST=https://api.bsky.app 2 + ATP_APPVIEW_HOST=https://api.blacksky.community
+1 -1
docs/build.md
··· 135 135 cd bskyweb/ 136 136 go mod tidy 137 137 go build -v -tags timetzdata -o bskyweb ./cmd/bskyweb 138 - ./bskyweb serve --appview-host=https://api.bsky.app 138 + ./bskyweb serve --appview-host=https://api.blacksky.community 139 139 ``` 140 140 141 141 On build success, access the application at [http://localhost:8100/](http://localhost:8100/). Subsequent changes require re-running the above steps in order to be reflected.
+1 -1
functions/profile/[handleOrDID].ts
··· 136 136 } 137 137 138 138 export async function onRequest(context) { 139 - const agent = new AtpAgent({service: 'https://api.bsky.app/'}) 139 + const agent = new AtpAgent({service: 'https://api.blacksky.community/'}) 140 140 const {request, env} = context 141 141 const origin = new URL(request.url).origin 142 142
+1 -1
functions/profile/[handleOrDID]/post/[rkey].ts
··· 198 198 } 199 199 200 200 export async function onRequest(context) { 201 - const agent = new AtpAgent({service: 'https://api.bsky.app/'}) 201 + const agent = new AtpAgent({service: 'https://api.blacksky.community/'}) 202 202 const {request, env} = context 203 203 const origin = new URL(request.url).origin 204 204 const {handleOrDID, rkey}: {handleOrDID: string; rkey: string} =
+2 -1
src/env/common.ts
··· 76 76 * The DID of the Bluesky appview to proxy to 77 77 */ 78 78 export const BLUESKY_PROXY_DID: Did = 79 - process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID || 'did:web:api.bsky.app' 79 + process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID || 'did:web:api.blacksky.community' 80 + export const ALT_PROXY_DID: Did = 'did:web:api.bsky.app' 80 81 81 82 /** 82 83 * The DID of the chat service to proxy to
+8 -2
src/lib/api/feed/utils.ts
··· 2 2 3 3 import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' 4 4 import {type UsePreferencesQueryResponse} from '#/state/queries/preferences' 5 - import {IS_WEB} from '#/env' 5 + import {ALT_PROXY_DID, IS_WEB} from '#/env' 6 + 7 + const TRENDING_FEED_DID = 'did:plc:qrz3lhbyuxbeilrc6nekdqme' 8 + const PROXY_TO_BLUESKY = `${ALT_PROXY_DID}#bsky_appview` 6 9 7 10 let debugTopics = '' 8 11 if (IS_WEB && typeof window !== 'undefined') { ··· 27 30 return BSKY_FEED_OWNER_DIDS.includes(uri.host) 28 31 } 29 32 30 - export function getProxyHeadersForFeed(_feedUri: string) { 33 + export function getProxyHeadersForFeed(feedUri: string) { 34 + if (feedUri.includes(TRENDING_FEED_DID)) { 35 + return {'atproto-proxy': PROXY_TO_BLUESKY} 36 + } 31 37 return {} 32 38 }
+97
src/lib/api/gatekeeper.ts
··· 1 + import {getGatekeeperUrl} from '#/lib/blacksky-pds' 2 + 3 + class GatekeeperError extends Error { 4 + constructor( 5 + public status: number, 6 + public errorType: string, 7 + message: string, 8 + ) { 9 + super(message) 10 + this.name = 'GatekeeperError' 11 + } 12 + } 13 + 14 + async function gatekeeperPost( 15 + serviceUrl: string, 16 + path: string, 17 + body: Record<string, unknown>, 18 + ): Promise<any> { 19 + const base = getGatekeeperUrl(serviceUrl) 20 + const res = await fetch(`${base}${path}`, { 21 + method: 'POST', 22 + headers: {'Content-Type': 'application/json'}, 23 + body: JSON.stringify(body), 24 + }) 25 + 26 + if (!res.ok) { 27 + let errorType = 'UnknownError' 28 + let message = `Request failed with status ${res.status}` 29 + try { 30 + const json = (await res.json()) as {error?: string; message?: string} 31 + errorType = json.error || errorType 32 + message = json.message || message 33 + } catch {} 34 + throw new GatekeeperError(res.status, errorType, message) 35 + } 36 + 37 + const text = await res.text() 38 + if (!text) return {} 39 + try { 40 + return JSON.parse(text) 41 + } catch { 42 + return {} 43 + } 44 + } 45 + 46 + export async function gateUpdateEmail(params: { 47 + serviceUrl: string 48 + did: string 49 + password: string 50 + email: string 51 + token?: string 52 + emailAuthFactor?: boolean 53 + }): Promise<{status: 'success' | 'tokenRequired'}> { 54 + const body: Record<string, unknown> = { 55 + did: params.did, 56 + password: params.password, 57 + email: params.email, 58 + } 59 + if (params.token) { 60 + body.token = params.token 61 + } 62 + if (params.emailAuthFactor !== undefined) { 63 + body.emailAuthFactor = params.emailAuthFactor 64 + } 65 + 66 + try { 67 + await gatekeeperPost(params.serviceUrl, '/gate/update-email', body) 68 + return {status: 'success'} 69 + } catch (e) { 70 + if (e instanceof GatekeeperError && e.errorType === 'TokenRequired') { 71 + return {status: 'tokenRequired'} 72 + } 73 + throw e 74 + } 75 + } 76 + 77 + export async function gateRequestAccountDelete(params: { 78 + serviceUrl: string 79 + did: string 80 + password: string 81 + }): Promise<void> { 82 + await gatekeeperPost(params.serviceUrl, '/gate/request-account-delete', { 83 + did: params.did, 84 + password: params.password, 85 + }) 86 + } 87 + 88 + export async function gateDeactivateAccount(params: { 89 + serviceUrl: string 90 + did: string 91 + password: string 92 + }): Promise<void> { 93 + await gatekeeperPost(params.serviceUrl, '/gate/deactivate-account', { 94 + did: params.did, 95 + password: params.password, 96 + }) 97 + }
+9
src/lib/blacksky-pds.ts
··· 1 + /** 2 + * Returns the gatekeeper base URL for a given PDS service URL. 3 + * The gatekeeper runs behind the same Caddy reverse proxy, so 4 + * the base URL is the same as the PDS service URL. 5 + */ 6 + export function getGatekeeperUrl(serviceUrl: string): string { 7 + // Strip trailing slash 8 + return serviceUrl.replace(/\/+$/, '') 9 + }
+7 -7
src/lib/constants.ts
··· 9 9 export const STAGING_SERVICE = 'https://staging.bsky.dev' 10 10 export const BSKY_SERVICE = 'https://blacksky.app' 11 11 export const BSKY_SERVICE_DID = 'did:web:bsky.social' 12 - export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app' 12 + export const PUBLIC_BSKY_SERVICE = 'https://api.blacksky.community' 13 13 export const DEFAULT_SERVICE = BSKY_SERVICE 14 14 export const HELP_DESK_URL = `https://github.com/blacksky-algorithms/blacksky.community/issues/new/choose` 15 15 export const EMBED_SERVICE = 'https://embed.bsky.app' ··· 167 167 168 168 export const MAX_LABELERS = 20 169 169 170 - export const VIDEO_SERVICE = 'https://video.bsky.app' 171 - export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app' 170 + export const VIDEO_SERVICE = 'https://video.blacksky.community' 171 + export const VIDEO_SERVICE_DID = 'did:web:video.blacksky.community' 172 172 173 - export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds 173 + export const VIDEO_MAX_DURATION_MS = 60 * 60 * 1000 // 60 minutes in milliseconds 174 174 /** 175 175 * Maximum size of a video in megabytes, _not_ mebibytes. Backend uses 176 176 * ISO megabytes. 177 177 */ 178 - export const VIDEO_MAX_SIZE = 1000 * 1000 * 100 // 100mb 178 + export const VIDEO_MAX_SIZE = 1000 * 1000 * 1000 * 5 // 5gb 179 179 180 180 export const SUPPORTED_MIME_TYPES = [ 181 181 'video/mp4', ··· 205 205 }, 206 206 } 207 207 208 - export const PUBLIC_APPVIEW = 'https://api.bsky.app' 209 - export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app' 208 + export const PUBLIC_APPVIEW = 'https://api.blacksky.community' 209 + export const PUBLIC_APPVIEW_DID = 'did:web:api.blacksky.community' 210 210 export const PUBLIC_STAGING_APPVIEW_DID = 'did:web:api.staging.bsky.dev' 211 211 212 212 export const DEV_ENV_APPVIEW = `http://localhost:2584` // always the same
+33
src/lib/hooks/useIsBlackskyPds.ts
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + import {useSession} from '#/state/session' 4 + 5 + const RQKEY_ROOT = 'gatekeeper-check' 6 + const RQKEY = (serviceUrl: string) => [RQKEY_ROOT, serviceUrl] 7 + 8 + /** 9 + * Returns true if the current user's PDS runs the pds-gatekeeper sidecar. 10 + * 11 + * Detection works by checking `/xrpc/_health` on the PDS -- the gatekeeper 12 + * intercepts this endpoint and adds a `gatekeeper_version` field to the 13 + * response. Any PDS running the gatekeeper is automatically detected. 14 + */ 15 + export function useIsBlackskyPds(): boolean { 16 + const {currentAccount} = useSession() 17 + const serviceUrl = currentAccount?.service ?? '' 18 + 19 + const {data} = useQuery({ 20 + queryKey: RQKEY(serviceUrl), 21 + queryFn: async () => { 22 + const base = serviceUrl.replace(/\/+$/, '') 23 + const res = await fetch(`${base}/xrpc/_health`) 24 + if (!res.ok) return false 25 + const json = (await res.json()) as {gatekeeper_version?: string} 26 + return typeof json.gatekeeper_version === 'string' 27 + }, 28 + staleTime: Infinity, 29 + enabled: !!serviceUrl, 30 + }) 31 + 32 + return data === true 33 + }
+9 -9
src/state/session/__tests__/session-test.ts
··· 26 26 "accounts": [], 27 27 "currentAgentState": { 28 28 "agent": { 29 - "service": "https://api.bsky.app/", 29 + "service": "https://api.blacksky.community/", 30 30 }, 31 31 "did": undefined, 32 32 }, ··· 115 115 ], 116 116 "currentAgentState": { 117 117 "agent": { 118 - "service": "https://api.bsky.app/", 118 + "service": "https://api.blacksky.community/", 119 119 }, 120 120 "did": undefined, 121 121 }, ··· 453 453 ], 454 454 "currentAgentState": { 455 455 "agent": { 456 - "service": "https://api.bsky.app/", 456 + "service": "https://api.blacksky.community/", 457 457 }, 458 458 "did": undefined, 459 459 }, ··· 515 515 ], 516 516 "currentAgentState": { 517 517 "agent": { 518 - "service": "https://api.bsky.app/", 518 + "service": "https://api.blacksky.community/", 519 519 }, 520 520 "did": undefined, 521 521 }, ··· 608 608 "accounts": [], 609 609 "currentAgentState": { 610 610 "agent": { 611 - "service": "https://api.bsky.app/", 611 + "service": "https://api.blacksky.community/", 612 612 }, 613 613 "did": undefined, 614 614 }, ··· 788 788 ], 789 789 "currentAgentState": { 790 790 "agent": { 791 - "service": "https://api.bsky.app/", 791 + "service": "https://api.blacksky.community/", 792 792 }, 793 793 "did": undefined, 794 794 }, ··· 1437 1437 ], 1438 1438 "currentAgentState": { 1439 1439 "agent": { 1440 - "service": "https://api.bsky.app/", 1440 + "service": "https://api.blacksky.community/", 1441 1441 }, 1442 1442 "did": undefined, 1443 1443 }, ··· 1503 1503 ], 1504 1504 "currentAgentState": { 1505 1505 "agent": { 1506 - "service": "https://api.bsky.app/", 1506 + "service": "https://api.blacksky.community/", 1507 1507 }, 1508 1508 "did": undefined, 1509 1509 }, ··· 1666 1666 ], 1667 1667 "currentAgentState": { 1668 1668 "agent": { 1669 - "service": "https://api.bsky.app/", 1669 + "service": "https://api.blacksky.community/", 1670 1670 }, 1671 1671 "did": undefined, 1672 1672 },