diff --git a/Dockerfile b/Dockerfile index d435cc1..dd2b005 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /repo/web COPY web/package.json web/package-lock.json ./ RUN npm ci COPY web/ . -COPY core/atproto_apps.json /repo/core/atproto_apps.json +COPY data/shared.json /repo/data/shared.json RUN npm run build FROM nginx:alpine diff --git a/Dockerfile.telnet b/Dockerfile.telnet index a26e9b6..dfc6dbf 100644 --- a/Dockerfile.telnet +++ b/Dockerfile.telnet @@ -4,5 +4,6 @@ COPY pyproject.toml . RUN pip install --no-cache-dir httpx COPY core/ core/ COPY telnet/ telnet/ +COPY data/ data/ EXPOSE 2323 CMD ["python", "-m", "telnet.server"] diff --git a/core/atproto_apps.json b/core/atproto_apps.json deleted file mode 100644 index 2b960d2..0000000 --- a/core/atproto_apps.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { "name": "Blacksky", "url": "https://blacksky.community" }, - { "name": "Bluesky", "url": "https://bsky.app" }, - { "name": "Grain Social", "url": "https://grain.social" }, - { "name": "Leaflet", "url": "https://leaflet.pub" }, - { "name": "pckt.blog", "url": "https://pckt.blog" }, - { "name": "Streamplace", "url": "https://stream.place" }, - { "name": "Tangled", "url": "https://tangled.sh" }, - { "name": "wisp.place", "url": "https://wisp.place" } -] diff --git a/core/atproto_apps.py b/core/atproto_apps.py index 982225a..15bf5e6 100644 --- a/core/atproto_apps.py +++ b/core/atproto_apps.py @@ -1,12 +1,10 @@ """Shared AT Protocol apps list — used by both the TUI and the web frontend.""" -import json import random -from pathlib import Path -_JSON_PATH = Path(__file__).parent / "atproto_apps.json" +from core.shared import ATPROTO_APPS -ATPROTO_APPS: list[dict[str, str]] = json.loads(_JSON_PATH.read_text()) +__all__ = ["ATPROTO_APPS", "pick_random_apps"] def pick_random_apps(count: int) -> list[dict[str, str]]: diff --git a/core/constellation.py b/core/constellation.py index 6e3efe6..125b6dd 100644 --- a/core/constellation.py +++ b/core/constellation.py @@ -2,8 +2,9 @@ import httpx from core import lexicon from core.models import BacklinkRef, BacklinksResponse +from core.shared import SERVICES -BASE_URL = "https://constellation.microcosm.blue/xrpc" +BASE_URL = SERVICES["constellation"] async def get_backlinks( diff --git a/core/lexicon.py b/core/lexicon.py index 56269b8..d46c3eb 100644 --- a/core/lexicon.py +++ b/core/lexicon.py @@ -1,11 +1,15 @@ """AT Protocol collection names for atbbs lexicons.""" -SITE = "xyz.atbbs.site" -BOARD = "xyz.atbbs.board" -POST = "xyz.atbbs.post" -BAN = "xyz.atbbs.ban" -HIDE = "xyz.atbbs.hide" -PIN = "xyz.atbbs.pin" -PROFILE = "xyz.atbbs.profile" +from core.shared import LEXICON_COLLECTIONS, OAUTH_BASE_SCOPES -OAUTH_SCOPE = f"atproto blob:*/* repo:{SITE} repo:{BOARD} repo:{POST} repo:{BAN} repo:{HIDE} repo:{PIN} repo:{PROFILE}" +SITE = LEXICON_COLLECTIONS["site"] +BOARD = LEXICON_COLLECTIONS["board"] +POST = LEXICON_COLLECTIONS["post"] +BAN = LEXICON_COLLECTIONS["ban"] +HIDE = LEXICON_COLLECTIONS["hide"] +PIN = LEXICON_COLLECTIONS["pin"] +PROFILE = LEXICON_COLLECTIONS["profile"] + +OAUTH_SCOPE = " ".join( + [*OAUTH_BASE_SCOPES, *(f"repo:{nsid}" for nsid in LEXICON_COLLECTIONS.values())] +) diff --git a/core/shared.py b/core/shared.py new file mode 100644 index 0000000..867fffa --- /dev/null +++ b/core/shared.py @@ -0,0 +1,33 @@ +"""Loader for the cross-language shared data file (data/shared.json). + +Dev checkout: reads from /data/shared.json. +Installed wheel: reads from core/_shared.json (bundled via hatch force-include). +""" + +import json +from pathlib import Path +from typing import Any + + +def _find_shared_json() -> Path: + here = Path(__file__).resolve().parent + candidates = [ + here / "_shared.json", + here.parent / "data" / "shared.json", + ] + for path in candidates: + if path.exists(): + return path + raise FileNotFoundError( + f"shared data file not found; looked in: {', '.join(str(p) for p in candidates)}" + ) + + +_DATA: dict[str, Any] = json.loads(_find_shared_json().read_text()) + +ATPROTO_APPS: list[dict[str, str]] = _DATA["atproto_apps"] +LEXICON_COLLECTIONS: dict[str, str] = _DATA["lexicon_collections"] +OAUTH_BASE_SCOPES: list[str] = _DATA["oauth_base_scopes"] +SERVICES: dict[str, str] = _DATA["services"] +DEFAULT_BOARD: dict[str, str] = _DATA["default_board"] +HANDLE_PLACEHOLDERS: list[str] = _DATA["handle_placeholders"] diff --git a/core/slingshot.py b/core/slingshot.py index 589aef9..edc2e08 100644 --- a/core/slingshot.py +++ b/core/slingshot.py @@ -4,8 +4,9 @@ import httpx from core.cache import TTLCache from core.models import AtUri, BacklinkRef, MiniDoc, Record +from core.shared import SERVICES -BASE_URL = "https://slingshot.microcosm.blue/xrpc" +BASE_URL = SERVICES["slingshot"] _identity_cache = TTLCache(ttl_seconds=300) # 5 minutes diff --git a/data/shared.json b/data/shared.json new file mode 100644 index 0000000..97195d9 --- /dev/null +++ b/data/shared.json @@ -0,0 +1,42 @@ +{ + "atproto_apps": [ + { "name": "Blacksky", "url": "https://blacksky.community" }, + { "name": "Bluesky", "url": "https://bsky.app" }, + { "name": "Grain Social", "url": "https://grain.social" }, + { "name": "Leaflet", "url": "https://leaflet.pub" }, + { "name": "pckt.blog", "url": "https://pckt.blog" }, + { "name": "Streamplace", "url": "https://stream.place" }, + { "name": "Tangled", "url": "https://tangled.sh" }, + { "name": "wisp.place", "url": "https://wisp.place" } + ], + "lexicon_collections": { + "site": "xyz.atbbs.site", + "board": "xyz.atbbs.board", + "post": "xyz.atbbs.post", + "ban": "xyz.atbbs.ban", + "hide": "xyz.atbbs.hide", + "pin": "xyz.atbbs.pin", + "profile": "xyz.atbbs.profile" + }, + "oauth_base_scopes": ["atproto", "blob:*/*"], + "services": { + "slingshot": "https://slingshot.microcosm.blue/xrpc", + "constellation": "https://constellation.microcosm.blue/xrpc", + "lightrail": "https://lightrail.microcosm.blue/xrpc" + }, + "default_board": { + "slug": "general", + "name": "General", + "description": "Whatever's on your mind." + }, + "handle_placeholders": [ + "handle.blacksky.app", + "handle.bsky.social", + "handle.eurosky.social", + "handle.northsky.social", + "handle.selfhosted.social", + "handle.tngl.sh", + "handle.pds.witchcraft.systems", + "handle.your-domain.com" + ] +} diff --git a/pyproject.toml b/pyproject.toml index 5fea535..db23732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,5 +24,8 @@ atbbs = "cli.__main__:main" [tool.hatch.build.targets.wheel] packages = ["cli", "core", "telnet", "tui"] +[tool.hatch.build.targets.wheel.force-include] +"data/shared.json" = "core/_shared.json" + [dependency-groups] dev = [] diff --git a/tui/screens/home.py b/tui/screens/home.py index 4e8d9c6..09c15f8 100644 --- a/tui/screens/home.py +++ b/tui/screens/home.py @@ -13,6 +13,7 @@ from tui.widgets.handle_input import HandleInput from core import lexicon from core.models import BBSNotFoundError, NetworkError, NoBBSError from core.resolver import resolve_bbs +from core.shared import SERVICES from core.slingshot import get_record, resolve_identities_batch from tui.screens.site import SiteScreen @@ -137,7 +138,7 @@ class HomeScreen(Screen): client = self.app.http_client try: resp = await client.get( - "https://lightrail.microcosm.blue/xrpc/com.atproto.sync.listReposByCollection", + f"{SERVICES['lightrail']}/com.atproto.sync.listReposByCollection", params={"collection": lexicon.SITE, "limit": 50}, ) if resp.status_code != 200: diff --git a/tui/screens/sysop/bbs_form.py b/tui/screens/sysop/bbs_form.py index 19aa86c..4b8e63e 100644 --- a/tui/screens/sysop/bbs_form.py +++ b/tui/screens/sysop/bbs_form.py @@ -9,14 +9,10 @@ from textual.containers import VerticalScroll from textual.widgets import Input, Static, TextArea from core import limits +from core.shared import DEFAULT_BOARD as _DEFAULT_BOARD from core.util import now_iso -DEFAULT_BOARD = { - "slug": "general", - "name": "General", - "description": "Whatever's on your mind.", - "created_at": "", -} +DEFAULT_BOARD = {**_DEFAULT_BOARD, "created_at": ""} class BBSFormMixin: diff --git a/tui/widgets/handle_input.py b/tui/widgets/handle_input.py index fe10135..6745818 100644 --- a/tui/widgets/handle_input.py +++ b/tui/widgets/handle_input.py @@ -2,16 +2,7 @@ from textual.widgets import Input -PLACEHOLDERS = [ - "handle.blacksky.app", - "handle.bsky.social", - "handle.eurosky.social", - "handle.northsky.social", - "handle.selfhosted.social", - "handle.tngl.sh", - "handle.pds.witchcraft.systems", - "handle.your-domain.com", -] +from core.shared import HANDLE_PLACEHOLDERS as PLACEHOLDERS class HandleInput(Input): diff --git a/web/docker-entrypoint.sh b/web/docker-entrypoint.sh index 0d597d1..0404b80 100755 --- a/web/docker-entrypoint.sh +++ b/web/docker-entrypoint.sh @@ -6,31 +6,13 @@ set -e # Strip trailing slash. PUBLIC_URL="${PUBLIC_URL%/}" -SCOPE="atproto blob:*/* repo:xyz.atbbs.site repo:xyz.atbbs.board repo:xyz.atbbs.post repo:xyz.atbbs.ban repo:xyz.atbbs.hide repo:xyz.atbbs.pin repo:xyz.atbbs.profile" +HTML=/usr/share/nginx/html -# Runtime config read by the SPA at startup. -cat > /usr/share/nginx/html/config.json < /usr/share/nginx/html/client-metadata.json < "${HTML}/config.json" +sed "s|__PUBLIC_URL__|${PUBLIC_URL}|g" \ + "${HTML}/client-metadata.template.json" > "${HTML}/client-metadata.json" exec nginx -g 'daemon off;' diff --git a/web/src/components/form/HandleInput.tsx b/web/src/components/form/HandleInput.tsx index cb7bd28..7603b3d 100644 --- a/web/src/components/form/HandleInput.tsx +++ b/web/src/components/form/HandleInput.tsx @@ -1,17 +1,7 @@ import { useEffect, useState, type InputHTMLAttributes } from "react"; +import { HANDLE_PLACEHOLDERS as PLACEHOLDERS } from "../../lib/shared"; import { inputStyles } from "./Form"; -const PLACEHOLDERS = [ - "handle.blacksky.app", - "handle.bsky.social", - "handle.eurosky.social", - "handle.northsky.social", - "handle.selfhosted.social", - "handle.tngl.sh", - "handle.pds.witchcraft.systems", - "handle.your-domain.com", -]; - interface HandleInputProps extends Omit< InputHTMLAttributes, "onChange" | "value" diff --git a/web/src/hooks/useDiscovery.ts b/web/src/hooks/useDiscovery.ts index f3c73b3..ee7a42a 100644 --- a/web/src/hooks/useDiscovery.ts +++ b/web/src/hooks/useDiscovery.ts @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { TTLCache } from "../lib/cache"; import { getRecord, resolveIdentitiesBatch } from "../lib/atproto"; import { SITE } from "../lib/lexicon"; +import { SERVICES } from "../lib/shared"; import { is } from "@atcute/lexicons/validations"; import { mainSchema as siteSchema } from "../lexicons/types/xyz/atbbs/site"; import type { XyzAtbbsSite } from "../lexicons"; @@ -33,7 +34,7 @@ export function useDiscovery(): DiscoveredBBS[] { (async () => { try { const response = await fetch( - `https://lightrail.microcosm.blue/xrpc/com.atproto.sync.listReposByCollection?collection=${SITE}&limit=50`, + `${SERVICES.lightrail}/com.atproto.sync.listReposByCollection?collection=${SITE}&limit=50`, ); const data = (await response.json()) as { repos: LightrailRepo[] }; if (!data.repos.length) return; diff --git a/web/src/lib/atproto.ts b/web/src/lib/atproto.ts index 444d90d..510dd34 100644 --- a/web/src/lib/atproto.ts +++ b/web/src/lib/atproto.ts @@ -1,10 +1,11 @@ /** Read-side wrappers for Slingshot and Constellation (no auth needed). */ import { TTLCache } from "./cache"; +import { SERVICES } from "./shared"; import { parseAtUri } from "./util"; -const SLINGSHOT = "https://slingshot.microcosm.blue/xrpc"; -const CONSTELLATION = "https://constellation.microcosm.blue/xrpc"; +const SLINGSHOT = SERVICES.slingshot; +const CONSTELLATION = SERVICES.constellation; export interface MiniDoc { did: string; diff --git a/web/src/lib/atprotoApps.ts b/web/src/lib/atprotoApps.ts index 8fc6d26..c205cde 100644 --- a/web/src/lib/atprotoApps.ts +++ b/web/src/lib/atprotoApps.ts @@ -1,15 +1,7 @@ -// App list shared with the Python TUI (see core/atproto_apps.py). -import apps from "../../../core/atproto_apps.json"; - -export interface AtprotoApp { - name: string; - url: string; -} - -export const ATPROTO_APPS: AtprotoApp[] = apps; +export { ATPROTO_APPS, type AtprotoApp } from "./shared"; +import { ATPROTO_APPS, type AtprotoApp } from "./shared"; export function pickRandomApps(count: number): AtprotoApp[] { - // Shuffle a copy of the list and take the first `count` entries. const shuffled = [...ATPROTO_APPS].sort(() => Math.random() - 0.5); return shuffled.slice(0, count); } diff --git a/web/src/lib/shared.ts b/web/src/lib/shared.ts new file mode 100644 index 0000000..9d5548c --- /dev/null +++ b/web/src/lib/shared.ts @@ -0,0 +1,35 @@ +import shared from "../../../data/shared.json"; + +export interface AtprotoApp { + name: string; + url: string; +} + +export interface LexiconCollections { + site: string; + board: string; + post: string; + ban: string; + hide: string; + pin: string; + profile: string; +} + +export interface Services { + slingshot: string; + constellation: string; + lightrail: string; +} + +export interface DefaultBoard { + slug: string; + name: string; + description: string; +} + +export const ATPROTO_APPS = shared.atproto_apps as AtprotoApp[]; +export const LEXICON_COLLECTIONS = + shared.lexicon_collections as LexiconCollections; +export const SERVICES = shared.services as Services; +export const DEFAULT_BOARD = shared.default_board as DefaultBoard; +export const HANDLE_PLACEHOLDERS = shared.handle_placeholders as string[]; diff --git a/web/src/pages/SysopCreate.tsx b/web/src/pages/SysopCreate.tsx index 6999b9e..ab7490e 100644 --- a/web/src/pages/SysopCreate.tsx +++ b/web/src/pages/SysopCreate.tsx @@ -3,6 +3,7 @@ import { useNavigate, useLoaderData } from "react-router-dom"; import { useAuth } from "../lib/auth"; import { putBoard, putSite } from "../lib/writes"; import { BOARD } from "../lib/lexicon"; +import { DEFAULT_BOARD } from "../lib/shared"; import { makeAtUri, nowIso } from "../lib/util"; import * as limits from "../lib/limits"; import { usePageTitle } from "../hooks/usePageTitle"; @@ -22,9 +23,9 @@ export default function SysopCreate() { const [intro, setIntro] = useState(""); const [boards, setBoards] = useState([ { - slug: "general", - name: "General", - desc: "Whatever's on your mind.", + slug: DEFAULT_BOARD.slug, + name: DEFAULT_BOARD.name, + desc: DEFAULT_BOARD.description, }, ]); const [error, setError] = useState(null); diff --git a/web/tsconfig.tsbuildinfo b/web/tsconfig.tsbuildinfo index 3eaee61..f9907bc 100644 --- a/web/tsconfig.tsbuildinfo +++ b/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/activitylist.tsx","./src/components/atprotoappscard.tsx","./src/components/bbspanel.tsx","./src/components/dialbbs.tsx","./src/components/discoverylist.tsx","./src/components/errorpage.tsx","./src/components/localtime.tsx","./src/components/loginmodal.tsx","./src/components/mythreadlist.tsx","./src/components/pinbutton.tsx","./src/components/pinnedlist.tsx","./src/components/form/boardroweditor.tsx","./src/components/form/composeform.tsx","./src/components/form/filechips.tsx","./src/components/form/form.tsx","./src/components/form/handleinput.tsx","./src/components/form/handlesuggestions.tsx","./src/components/form/loginform.tsx","./src/components/layout/footer.tsx","./src/components/layout/header.tsx","./src/components/layout/headerbreadcrumbs.tsx","./src/components/layout/layout.tsx","./src/components/layout/logo.tsx","./src/components/layout/mobilebackbutton.tsx","./src/components/layout/mobilemenu.tsx","./src/components/nav/actionbar.tsx","./src/components/nav/actionbutton.tsx","./src/components/nav/listlink.tsx","./src/components/nav/pagenav.tsx","./src/components/nav/threadlink.tsx","./src/components/post/attachmentlink.tsx","./src/components/post/newscard.tsx","./src/components/post/postactions.tsx","./src/components/post/postbody.tsx","./src/components/post/postmeta.tsx","./src/components/post/replycard.tsx","./src/components/post/threadcard.tsx","./src/components/profile/editprofile.tsx","./src/components/profile/viewprofile.tsx","./src/hooks/usebreadcrumb.tsx","./src/hooks/usediscovery.ts","./src/hooks/usedropdown.ts","./src/hooks/usehandlesearch.ts","./src/hooks/usepagetitle.ts","./src/hooks/useresolvedbbs.ts","./src/hooks/usethreadreplies.ts","./src/lexicons/index.ts","./src/lexicons/types/xyz/atbbs/ban.ts","./src/lexicons/types/xyz/atbbs/board.ts","./src/lexicons/types/xyz/atbbs/hide.ts","./src/lexicons/types/xyz/atbbs/pin.ts","./src/lexicons/types/xyz/atbbs/post.ts","./src/lexicons/types/xyz/atbbs/profile.ts","./src/lexicons/types/xyz/atbbs/site.ts","./src/lib/activity.ts","./src/lib/atproto.ts","./src/lib/atprotoapps.ts","./src/lib/auth.ts","./src/lib/bbs.ts","./src/lib/bsky.ts","./src/lib/cache.ts","./src/lib/deletebbs.ts","./src/lib/lexicon.ts","./src/lib/limits.ts","./src/lib/loginmodal.tsx","./src/lib/mythreads.ts","./src/lib/pins.ts","./src/lib/profile.ts","./src/lib/replies.ts","./src/lib/util.ts","./src/lib/writes.ts","./src/pages/bbs.tsx","./src/pages/board.tsx","./src/pages/dashboard.tsx","./src/pages/home.tsx","./src/pages/loggedouthome.tsx","./src/pages/news.tsx","./src/pages/notfound.tsx","./src/pages/oauthcallback.tsx","./src/pages/profile.tsx","./src/pages/sysopcreate.tsx","./src/pages/sysopedit.tsx","./src/pages/sysopmoderate.tsx","./src/pages/thread.tsx","./src/router/routes.tsx","./src/router/loaders/account.ts","./src/router/loaders/auth.ts","./src/router/loaders/bbs.ts","./src/router/loaders/board.ts","./src/router/loaders/home.ts","./src/router/loaders/index.ts","./src/router/loaders/profile.ts","./src/router/loaders/sysop.ts","./src/router/loaders/thread.ts"],"version":"6.0.2"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/activitylist.tsx","./src/components/atprotoappscard.tsx","./src/components/bbspanel.tsx","./src/components/dialbbs.tsx","./src/components/discoverylist.tsx","./src/components/errorpage.tsx","./src/components/localtime.tsx","./src/components/loginmodal.tsx","./src/components/mythreadlist.tsx","./src/components/pinbutton.tsx","./src/components/pinnedlist.tsx","./src/components/form/boardroweditor.tsx","./src/components/form/composeform.tsx","./src/components/form/filechips.tsx","./src/components/form/form.tsx","./src/components/form/handleinput.tsx","./src/components/form/handlesuggestions.tsx","./src/components/form/loginform.tsx","./src/components/layout/footer.tsx","./src/components/layout/header.tsx","./src/components/layout/headerbreadcrumbs.tsx","./src/components/layout/layout.tsx","./src/components/layout/logo.tsx","./src/components/layout/mobilebackbutton.tsx","./src/components/layout/mobilemenu.tsx","./src/components/nav/actionbar.tsx","./src/components/nav/actionbutton.tsx","./src/components/nav/listlink.tsx","./src/components/nav/pagenav.tsx","./src/components/nav/threadlink.tsx","./src/components/post/attachmentlink.tsx","./src/components/post/newscard.tsx","./src/components/post/postactions.tsx","./src/components/post/postbody.tsx","./src/components/post/postmeta.tsx","./src/components/post/replycard.tsx","./src/components/post/threadcard.tsx","./src/components/profile/editprofile.tsx","./src/components/profile/viewprofile.tsx","./src/hooks/usebreadcrumb.tsx","./src/hooks/usediscovery.ts","./src/hooks/usedropdown.ts","./src/hooks/usehandlesearch.ts","./src/hooks/usepagetitle.ts","./src/hooks/useresolvedbbs.ts","./src/hooks/usethreadreplies.ts","./src/lexicons/index.ts","./src/lexicons/types/xyz/atbbs/ban.ts","./src/lexicons/types/xyz/atbbs/board.ts","./src/lexicons/types/xyz/atbbs/hide.ts","./src/lexicons/types/xyz/atbbs/pin.ts","./src/lexicons/types/xyz/atbbs/post.ts","./src/lexicons/types/xyz/atbbs/profile.ts","./src/lexicons/types/xyz/atbbs/site.ts","./src/lib/activity.ts","./src/lib/atproto.ts","./src/lib/atprotoapps.ts","./src/lib/auth.ts","./src/lib/bbs.ts","./src/lib/bsky.ts","./src/lib/cache.ts","./src/lib/deletebbs.ts","./src/lib/lexicon.ts","./src/lib/limits.ts","./src/lib/loginmodal.tsx","./src/lib/mythreads.ts","./src/lib/pins.ts","./src/lib/profile.ts","./src/lib/replies.ts","./src/lib/shared.ts","./src/lib/util.ts","./src/lib/writes.ts","./src/pages/bbs.tsx","./src/pages/board.tsx","./src/pages/dashboard.tsx","./src/pages/home.tsx","./src/pages/loggedouthome.tsx","./src/pages/news.tsx","./src/pages/notfound.tsx","./src/pages/oauthcallback.tsx","./src/pages/profile.tsx","./src/pages/sysopcreate.tsx","./src/pages/sysopedit.tsx","./src/pages/sysopmoderate.tsx","./src/pages/thread.tsx","./src/router/routes.tsx","./src/router/loaders/account.ts","./src/router/loaders/auth.ts","./src/router/loaders/bbs.ts","./src/router/loaders/board.ts","./src/router/loaders/home.ts","./src/router/loaders/index.ts","./src/router/loaders/profile.ts","./src/router/loaders/sysop.ts","./src/router/loaders/thread.ts"],"version":"6.0.2"} \ No newline at end of file diff --git a/web/vite.config.ts b/web/vite.config.ts index c84e95d..ae215b3 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,22 +1,19 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import shared from "../data/shared.json"; const SERVER_HOST = "127.0.0.1"; const SERVER_PORT = 5173; const SCOPE = [ - "atproto", - "blob:*/*", - "repo:xyz.atbbs.site", - "repo:xyz.atbbs.board", - "repo:xyz.atbbs.post", - "repo:xyz.atbbs.ban", - "repo:xyz.atbbs.hide", - "repo:xyz.atbbs.pin", - "repo:xyz.atbbs.profile", + ...shared.oauth_base_scopes, + ...Object.values(shared.lexicon_collections).map((nsid) => `repo:${nsid}`), ].join(" "); +// Placeholder the Docker entrypoint replaces at runtime with PUBLIC_URL. +const PUBLIC_URL_TOKEN = "__PUBLIC_URL__"; + interface ClientMetadata { client_id: string; client_name: string; @@ -46,6 +43,15 @@ function buildMetadata(publicUrl: string): ClientMetadata { }; } +function buildConfig(publicUrl: string) { + const u = publicUrl.replace(/\/$/, ""); + return { + client_id: `${u}/client-metadata.json`, + redirect_uri: `${u}/oauth/callback`, + scope: SCOPE, + }; +} + /** * Dev: synthesizes a loopback client_id (atproto OAuth forbids `localhost`, * so the redirect goes to 127.0.0.1). @@ -53,9 +59,9 @@ function buildMetadata(publicUrl: string): ClientMetadata { * Build with VITE_PUBLIC_URL: emits config.json + client-metadata.json for * static deploys (Cloudflare Pages, etc.). * - * Build without VITE_PUBLIC_URL: produces a generic bundle. The Docker - * entrypoint generates config.json + client-metadata.json at runtime from - * the PUBLIC_URL env var. + * Build without VITE_PUBLIC_URL: emits *.template.json files with a + * __PUBLIC_URL__ token. The Docker entrypoint substitutes at runtime from + * the PUBLIC_URL env var. NSIDs/scope live in data/shared.json only. */ export default defineConfig(({ command }) => { const isBuild = command === "build"; @@ -71,35 +77,37 @@ export default defineConfig(({ command }) => { process.env.VITE_OAUTH_SCOPE = SCOPE; } - // Static deploy: emit config.json and client-metadata.json at build time. - let staticFiles: Array<{ fileName: string; source: string }> = []; - if (isBuild && publicUrl) { - if (!publicUrl.startsWith("https://")) { - throw new Error( - `VITE_PUBLIC_URL must use https:// (got ${publicUrl}).`, + const staticFiles: Array<{ fileName: string; source: string }> = []; + if (isBuild) { + if (publicUrl) { + if (!publicUrl.startsWith("https://")) { + throw new Error( + `VITE_PUBLIC_URL must use https:// (got ${publicUrl}).`, + ); + } + staticFiles.push( + { + fileName: "client-metadata.json", + source: JSON.stringify(buildMetadata(publicUrl), null, 2) + "\n", + }, + { + fileName: "config.json", + source: JSON.stringify(buildConfig(publicUrl), null, 2) + "\n", + }, + ); + } else { + staticFiles.push( + { + fileName: "client-metadata.template.json", + source: + JSON.stringify(buildMetadata(PUBLIC_URL_TOKEN), null, 2) + "\n", + }, + { + fileName: "config.template.json", + source: JSON.stringify(buildConfig(PUBLIC_URL_TOKEN), null, 2) + "\n", + }, ); } - const u = publicUrl.replace(/\/$/, ""); - const metadata = buildMetadata(u); - staticFiles = [ - { - fileName: "client-metadata.json", - source: JSON.stringify(metadata, null, 2) + "\n", - }, - { - fileName: "config.json", - source: - JSON.stringify( - { - client_id: metadata.client_id, - redirect_uri: metadata.redirect_uris[0], - scope: SCOPE, - }, - null, - 2, - ) + "\n", - }, - ]; } return { @@ -118,7 +126,7 @@ export default defineConfig(({ command }) => { server: { host: SERVER_HOST, port: SERVER_PORT, - // Allow importing ../core/atproto_apps.json (shared with the Python TUI). + // Allow importing ../../data/shared.json (shared with the Python TUI). fs: { allow: [".."] }, }, };