diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcb2719 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +repos +.claude diff --git a/README.md b/README.md index e69de29..a6bab35 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,48 @@ +# at + +- https://github.com/bluesky-social/atproto +- https://github.com/bluesky-social/atproto/discussions/2026 + +|word|name|example| +|---|---|---| +|at|uri|at://example.com| +|@|user|@example.com| +|[at]proto|repo|`git@github.com:bluesky-social/atproto`| +|[at]mosphere|system|pds, bsky(appview), ozone, bgs, plc| +|[a]uthenticated [t]ransfer|protocol|[did](https://www.w3.org/TR/did-core/)| + +- https://atproto.com/ja/guides/glossary + +## account + +- [ai@syu.is](https://syu.is/profile/did:plc:6qyecktefllvenje24fcxnie) +- [ai@bsky.app](https://bsky.app/profile/did:plc:6qyecktefllvenje24fcxnie) +- https://plc.syu.is/did:plc:6qyecktefllvenje24fcxnie +- https://plc.directory/did:plc:6qyecktefllvenje24fcxnie + +```sh +$ curl -sL syu.is/xrpc/_health + +# latest +# https://github.com/bluesky-social/atproto/blob/main/packages/pds/package.json +$ curl -sL https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/packages/pds/package.json |jq -r .version +``` + +```sh +$ handle=ai.syui.ai +$ curl -sL "syu.is/xrpc/com.atproto.repo.describeRepo?repo=${handle}" |jq -r .did +did:plc:6qyecktefllvenje24fcxnie + +$ curl -sL "syu.is/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=app.bsky.feed.post&reverse=true&limit=1" +{"records":[{"uri":"at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.post/3l6s2riuouk2j","cid":"bafyreibjohl7va4upkibw5twaxdd4jg3l6rmfatu4dpjjfd5xkb2ijtlx4","value":{"text":"hello","$type":"app.bsky.feed.post","langs":["ja"],"createdAt":"2024-10-18T13:21:39.809Z"}}],"cursor":"3l6s2riuouk2j"} +``` + +## feed + +> at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/cmd + +- https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd +- https://feed.syu.is/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:4hqjfn7m6n5hno3doamuhgef/app.bsky.feed.generator/cmd +- https://desc.syu.is/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/cmd + + diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..22fda73 --- /dev/null +++ b/compose.yml @@ -0,0 +1,139 @@ +services: + + database: + image: postgres:16-alpine + restart: always + env_file: + - ./envs/postgres + volumes: + - ./configs/postgres/init/:/docker-entrypoint-initdb.d/ + - ./data/postgres/:/var/lib/postgresql/data/ + healthcheck: + test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" + interval: 5s + retries: 20 + + redis: + image: redis:alpine + restart: always + volumes: + - ./data/redis/:/data/ + healthcheck: + test: ["CMD", "redis-cli", "ping", "|", "grep", "PONG"] + interval: 1s + timeout: 5s + retries: 5 + + plc: + ports: + - 2582:3000 + build: + context: ./repos/did-method-plc/ + dockerfile: packages/server/Dockerfile + restart: always + env_file: + - ./envs/plc + depends_on: + database: + condition: service_healthy + + pds: + ports: + - 2583:3000 + build: + context: ./repos/atproto/ + dockerfile: services/pds/Dockerfile + restart: always + env_file: + - ./envs/pds + volumes: + - ./data/pds/:/data/ + command: node --enable-source-maps index.js + depends_on: + database: + condition: service_healthy + + bsky: + ports: + - 2584:2584 + build: + context: ./repos/atproto/ + dockerfile: services/bsky/Dockerfile + restart: always + env_file: + - ./envs/bsky + user: root + volumes: + - ./data/bsky/:/data/ + command: node --enable-source-maps api.js + depends_on: + database: + condition: service_healthy + redis: + condition: service_healthy + + bgs: + ports: + - 2470:2470 + build: + context: ./repos/indigo/ + dockerfile: cmd/bigsky/Dockerfile + restart: always + env_file: + - ./envs/bgs + volumes: + - ./data/bgs/:/data/ + depends_on: + database: + condition: service_healthy + + social-app: + ports: + - 8100:8100 + build: + context: ./repos/social-app/ + dockerfile: Dockerfile + restart: always + env_file: + - ./envs/social-app + command: "/usr/bin/bskyweb serve" + + jetstream: + build: + context: ./repos/jetstream/ + dockerfile: Dockerfile + ports: + - 6008:6008 + volumes: + - ./data/jetstream:/data + restart: always + env_file: + - ./envs/jetstream + + ozone-web: + build: + context: ./repos/ozone/ + ports: + - 2586:3000 + restart: always + volumes: + - ./data/ozone/:/data/ + env_file: + - ./envs/ozone + depends_on: + database: + condition: service_healthy + + ozone: + build: + context: ./repos/atproto/ + dockerfile: services/ozone/Dockerfile + ports: + - 2585:3000 + restart: always + command: node --enable-source-maps api.js + volumes: + - ./data/ozone/:/data/ + env_file: + - ./envs/ozone + diff --git a/configs/postgres/init/init.sql b/configs/postgres/init/init.sql new file mode 100644 index 0000000..f86b7c9 --- /dev/null +++ b/configs/postgres/init/init.sql @@ -0,0 +1,35 @@ +-- PLC +CREATE DATABASE plc; +GRANT ALL PRIVILEGES ON DATABASE plc TO postgres; + +-- BGS +CREATE DATABASE bgs; +CREATE DATABASE carstore; +GRANT ALL PRIVILEGES ON DATABASE bgs TO postgres; +GRANT ALL PRIVILEGES ON DATABASE carstore TO postgres; + +-- bsky(AppView) +--CREATE DATABASE appview; +--GRANT ALL PRIVILEGES ON DATABASE appview TO postgres; +CREATE DATABASE bsky; +GRANT ALL PRIVILEGES ON DATABASE bsky TO postgres; + +-- ozone(Moderation) +--CREATE DATABASE moderation; +--GRANT ALL PRIVILEGES ON DATABASE moderation TO postgres; +CREATE DATABASE ozone; +GRANT ALL PRIVILEGES ON DATABASE ozone TO postgres; + +-- search(palomar) +CREATE DATABASE search; +GRANT ALL PRIVILEGES ON DATABASE search TO postgres; +--CREATE DATABASE palomar; +--GRANT ALL PRIVILEGES ON DATABASE palomar TO postgres; + +-- PDS +CREATE DATABASE pds; +GRANT ALL PRIVILEGES ON DATABASE pds TO postgres; + +-- BSYNC +CREATE DATABASE bsync; +GRANT ALL PRIVILEGES ON DATABASE bsync TO postgres; diff --git a/envs/bgs b/envs/bgs new file mode 100644 index 0000000..a5097f3 --- /dev/null +++ b/envs/bgs @@ -0,0 +1,6 @@ +DATABASE_URL=postgres://postgres:postgres@database/bgs +CARSTORE_DATABASE_URL=postgres://postgres:postgres@database/carstore +DATA_DIR=/data +ATP_PLC_HOST=https://plc.${host} +BGS_NEW_PDS_PER_DAY_LIMIT=1000 +BGS_ADMIN_KEY= diff --git a/envs/bsky b/envs/bsky new file mode 100644 index 0000000..5bebd98 --- /dev/null +++ b/envs/bsky @@ -0,0 +1,19 @@ +BSKY_PORT=2584 +BSKY_BLOB_CACHE_LOC=/data/ +BSKY_BSYNC_HTTP_VERSION=1.1 +BSKY_BSYNC_PORT=3002 +BSKY_BSYNC_URL=http://bsky:3002 +BSKY_COURIER_URL=http://fake-courier.example.invalid/ +BSKY_DATAPLANE_HTTP_VERSION=1.1 +BSKY_DATAPLANE_PORT=3001 +BSKY_DATAPLANE_URLS=http://bsky:3001 +BSKY_DB_POSTGRES_URL=postgres://postgres:postgres@database/bsky +BSKY_DID_PLC_URL=https://plc.${host} +BSKY_PUBLIC_URL=https://bsky.${host} +BSKY_REPO_PROVIDER=wss://bgs.${host} +BSKY_SERVER_DID=did:web:bsky.${host} +MOD_SERVICE_DID=did:web:ozone.${host} + +#BSKY_IMG_URI_ENDPOINT=https://bsky.${host}/img +BSKY_ADMIN_PASSWORDS +BSKY_SERVICE_SIGNING_KEY diff --git a/envs/jetstream b/envs/jetstream new file mode 100644 index 0000000..324bbd7 --- /dev/null +++ b/envs/jetstream @@ -0,0 +1,5 @@ +JETSTREAM_WS_URL=wss://bgs.${host}/xrpc/com.atproto.sync.subscribeRepos +JETSTREAM_DATA_DIR=/data +JETSTREAM_LISTEN_ADDR=:6008 +JETSTREAM_METRICS_LISTEN_ADDR=:6009 +JETSTREAM_LIVENESS_TTL=96h diff --git a/envs/ozone b/envs/ozone new file mode 100644 index 0000000..72e89eb --- /dev/null +++ b/envs/ozone @@ -0,0 +1,29 @@ +OZONE_SERVER_DID=did:web:ozone.${host} +OZONE_PUBLIC_URL=https://ozone.${host} +OZONE_ADMIN_HANDLE=${user}.${host} +OZONE_MODERATOR_DIDS=${did} +OZONE_ADMIN_DIDS=${did} +OZONE_DB_POSTGRES_URL=postgres://postgres:postgres@database/ozone +OZONE_DID_PLC_URL=https://plc.${host} +NEXT_PUBLIC_PLC_DIRECTORY_URL=https://plc.${host} +NEXT_PUBLIC_OZONE_SERVICE_DID=did:web:ozone.${host} +NEXT_PUBLIC_SOCIAL_APP_DOMAIN=mod.${host} +NEXT_PUBLIC_SOCIAL_APP_URL=https://mod.${host} +OZONE_APPVIEW_DID=did:web:bsky.${host} +OZONE_APPVIEW_URL=https://bsky.${host} +OZONE_APPVIEW_PUSH_EVENTS=false +OZONE_PDS_DID=did:web:${host} +OZONE_PDS_URL=https://${host} +OZONE_DEV_MODE=true +OZONE_DB_MIGRATE=1 + +OZONE_ADMIN_PASSWORD +OZONE_SIGNING_KEY_HEX +OZONE_BLOB_DIVERT_ADMIN_PASSWORD +OZONE_VERIFIER_URL +OZONE_VERIFIER_DID +OZONE_VERIFIER_PASSWORD +OZONE_VERIFIER_ISSUERS_TO_INDEX +OZONE_VERIFIER_JETSTREAM_URL + +OZONE_APPVIEW_PUSH_EVENTS=true diff --git a/envs/pds b/envs/pds new file mode 100644 index 0000000..b2f0241 --- /dev/null +++ b/envs/pds @@ -0,0 +1,23 @@ +PDS_HOSTNAME=${host} +PDS_DB_POSTGRES_URL=postgres://postgres:postgres@database/pds +PDS_DATA_DIRECTORY=/data +PDS_BLOBSTORE_DISK_LOCATION=/data/img/static +#PDS_BLOBSTORE_DISK_TMP_LOCATION=/data/img/tmp +PDS_BSKY_APP_VIEW_DID=did:web:bsky.${host} +PDS_BSKY_APP_VIEW_URL=https://bsky.${host} +PDS_CRAWLERS=https://bgs.${host} +PDS_SEQUENCER_ENABLED=true +PDS_SEQUENCER_DB_LOCATION=/data/sequencer.sqlite +PDS_DEV_MODE=true +PDS_DID_PLC_URL=https://plc.${host} +PDS_ENABLE_DID_DOC_WITH_SESSION=true +PDS_INVITE_INTERVAL=604800000 +PDS_SERVICE_DID=did:web:${host} +PDS_EMAIL_FROM_ADDRESS=no-reply@${host} + +PDS_INVITE_REQUIRED=true +PDS_EMAIL_SMTP_URL=smtps://${user}:${app_password}@smtp.gmail.com +PDS_ADMIN_PASSWORD +PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX +PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX +PDS_JWT_SECRET diff --git a/envs/plc b/envs/plc new file mode 100644 index 0000000..f0ccb52 --- /dev/null +++ b/envs/plc @@ -0,0 +1,4 @@ +DATABASE_URL=postgres://postgres:postgres@database/plc +DB_CREDS_JSON='{"username":"postgres","password":"postgres","host":"database","port":"5432","database":"plc"}' +ENABLE_MIGRATIONS=true +DB_MIGRATE_CREDS_JSON='{"username":"postgres","password":"postgres","host":"database","port":"5432","database":"plc"}' diff --git a/envs/postgres b/envs/postgres new file mode 100644 index 0000000..4184165 --- /dev/null +++ b/envs/postgres @@ -0,0 +1,4 @@ +#POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=healthcheck diff --git a/envs/social-app b/envs/social-app new file mode 100644 index 0000000..a1bc28f --- /dev/null +++ b/envs/social-app @@ -0,0 +1,3 @@ +ATP_APPVIEW_HOST=https://public.api.bsky.app +EXPO_PUBLIC_BLUESKY_PROXY_DID=did:web:api.bsky.app +EXPO_PUBLIC_ENV=production diff --git a/icons/Logotype.tsx b/icons/Logotype.tsx new file mode 100644 index 0000000..b087a56 --- /dev/null +++ b/icons/Logotype.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import Svg, {Path, SvgProps, PathProps} from 'react-native-svg' + +import {usePalette} from '#/lib/hooks/usePalette' + +const ratio = 17 / 64 + +export function Logotype({ + fill, + ...rest +}: {fill?: PathProps['fill']} & SvgProps) { + const pal = usePalette('default') + // @ts-ignore it's fiiiiine + const size = parseInt(rest.width || 32) + + return ( + + + + + + + + + + + + ) +} diff --git a/icons/title.svg b/icons/title.svg new file mode 100644 index 0000000..73a0b2f --- /dev/null +++ b/icons/title.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/install.zsh b/install.zsh new file mode 100755 index 0000000..54a91f1 --- /dev/null +++ b/install.zsh @@ -0,0 +1,328 @@ +#!/bin/zsh + +# ./install.zsh $HOST +repos_v='{}' +function at-repos-env() { + host=$1 + if [ -z "$1" ];then + host=syu.is + fi + did=did:plc:6qyecktefllvenje24fcxnie + icon=https://git.syui.ai/ai/at/raw/branch/main/icons/Logotype.tsx + repos=( + https://github.com/did-method-plc/did-method-plc + https://github.com/bluesky-social/indigo + https://github.com/bluesky-social/atproto + https://github.com/bluesky-social/social-app + https://github.com/bluesky-social/feed-generator + https://github.com/bluesky-social/ozone + https://github.com/bluesky-social/jetstream + ) + services=( bsky plc pds jetstream bgs ozone social-app ) + d=${0:a:h} + dh=${0:a:h:h} + name=${host%%.*} + domain=${host##*.} + dport=5000 +} + +function at-repos-json() { + f=~/.config/atproto/token.json + j="{ \"did\": \"did:plc:6qyecktefllvenje24fcxnie\", \"didDoc\": { \"service\": [ { \"serviceEndpoint\": \"https://syu.is\" } ] }, \"handle\": \"ai.syu.is\", \"accessJwt\": \"xxx\" }" + if [ ! -f "$f" ];then + mkdir -p ~/.config/atproto + echo $j >> $f + fi + echo $f +} + +function at-repos-token() { + at-repos-json + if [ -z "$host" ] && [ -f $f ];then + host=`cat $f|jq -r ".didDoc.service.[].serviceEndpoint"` + handle=`cat $f|jq -r ".handle"` + did=`cat $f|jq -r ".did"` + token=`cat $f|jq -r ".token"` + host=${host##*/} + fi + name=${host%%.*} + domain=${host##*.} +} + +function at-repos-clone() { + if [ ! -d $d/repos ];then + mkdir -p $d/repos + fi + cd $d/repos + for ((i=1; i<=${#repos}; i++)); do + repo=${repos[$i]} + echo $repo + if [ ! -d $d/repos/${repo##*/} ];then + git clone $repo + + fi + done + if [ ! -f $d/repos/feed-generator/Dockerfile ] && [ -f $d/docker/feed/Dockerfile ];then + cp -rf $d/docker/feed/Dockerfile $d/repos/feed-generator/ + fi +} + +function at-repos-pull() { + cd $d/repos + for ((i=1; i<=${#repos}; i++)); do + repo=${repos[$i]} + echo $repo + if [ -d $d/repos/${repo##*/} ];then + cd $d/repos/${repo##*/} + git stash + if ! git pull;then + rm -rf $d/repos/${repo##*/} + at-repos-clone + fi + fi + rv=$(echo "$repos_v" | jq -r ".[\"${repo##*/}\"]") + if [ "$rv" != "null" ];then + cd $d/repos/${repo##*/} + git reset --hard $rv + cd .. + fi + done + cd $d +} + +function at-repos-social-app-icon() { + curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/main/src/view/icons/Logotype.tsx -o $d/repos/social-app/src/view/icons/Logotype.tsx + if [ -d $d/icons ];then + mkdir -p $d/icons + fi + cp -rf $d/repos/social-app/src/view/icons/Logotype.tsx $d/icons/ +} + +function at-repos-social-app-icon-origin() { + curl -sL $icon -o $d/icons/Logotype.tsx +} + +function at-repos-social-app-avatar-write() { + did_admin=did:plc:6qyecktefllvenje24fcxnie + dt=$d/repos/social-app/src + cd $dt + f=$dt/lib/constants.ts + sed -i "s#export const BSKY_SERVICE = 'https://bsky.social'#export const BSKY_SERVICE = 'https://${host}'#g" $f + sed -i "s#export const BSKY_SERVICE_DID = 'did:web:bsky.social'#export const BSKY_SERVICE_DID = 'did:web:${host}'#g" $f + sed -i "s#export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'#export const PUBLIC_BSKY_SERVICE = 'https://bsky.${host}'#g" $f + sed -i "s#export const PUBLIC_APPVIEW = 'https://api.bsky.app'#export const PUBLIC_APPVIEW = 'https://bsky.${host}'#g" $f + sed -i "s#export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app'#export const PUBLIC_APPVIEW_DID = 'did:web:bsky.${host}'#g" $f + + # Disable external services (CORS fix) + f=$dt/state/geolocation/const.ts + curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/state/geolocation/const.ts -o $f + cat > $f << 'GEOEOF' +import {type GeolocationStatus} from '#/state/geolocation/types' +import {BAPP_CONFIG_DEV_URL, IS_DEV} from '#/env' +import {type Device} from '#/storage' + +export const IPCC_URL = `https://bsky.app/ipcc` +// Disabled for self-hosted environment to avoid CORS errors +export const BAPP_CONFIG_URL_PROD = null +export const BAPP_CONFIG_URL = null +export const GEOLOCATION_CONFIG_URL = BAPP_CONFIG_URL + +export const DEFAULT_GEOLOCATION_CONFIG: Device['geolocation'] = { + countryCode: undefined, + regionCode: undefined, + ageRestrictedGeos: [], + ageBlockedGeos: [], +} + +export const DEFAULT_GEOLOCATION_STATUS: GeolocationStatus = { + countryCode: undefined, + regionCode: undefined, + isAgeRestrictedGeo: false, + isAgeBlockedGeo: false, +} +GEOEOF + + # Add null check to geolocation config.ts to prevent fetch(null) errors + f=$dt/state/geolocation/config.ts + curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/state/geolocation/config.ts -o $f + # Add null check at the beginning of getGeolocationConfig function (after line with 'url: string,') + sed -i "s/): Promise {/): Promise {\n if (!url) return undefined/" $f + + # Disable Statsig (CORS fix) + f=$dt/lib/statsig/statsig.tsx + sed -i "s#api: 'https://events.bsky.app/v2'#api: '' // Disabled for self-hosted#g" $f + # Disable SDK initialization to prevent statsigapi.net connections + sed -i "s#const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV'#const SDK_KEY = '' // Disabled for self-hosted#g" $f + + f=$dt/view/icons/Logotype.tsx + o=$d/icons/Logotype.tsx + cp -rf $o $f + + f=$dt/view/com/util/UserAvatar.tsx + curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/view/com/util/UserAvatar.tsx -o $f + sed -i "s#/img/avatar/plain/#https://cdn.web.syu.is/img/avatar/plain/#g" $f + sed -i "s#/img/avatar_thumbnail/plain/#https://bsky.${host}/img/avatar/plain/#g" $f + sed -i "s#source={{uri: avatar}}#source={{ uri: hackModifyThumbnailPath(avatar, 1 > 0), }}#g" $f + curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/lib/strings/url-helpers.ts -o $dt/lib/strings/url-helpers.ts + sed -i "s#https://go.web.syu.is/redirect?u=\${encodeURIComponent(url)}#\${url}#g" $dt/lib/strings/url-helpers.ts + grep -R $did_admin .|cut -d : -f 1|sort -u|xargs sed -i "s/${did_admin}/${did}/g" +} + +function at-repos-atproto-service-bsky-api-patch() { + # https://github.com/itaru2622/bluesky-selfhost-env/blob/master/patching/105-atproto-services-for-docker.diff + f=$d/repos/atproto/services/bsky/api.js + curl -sL https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/bsky/api.js -o $f + d_=$d/repos/atproto + p_=$d/patching/4367-atproto-services-bsky-api.diff + echo "applying patch: under ${f} for ${p_}" + pushd ${d_} + patch -p1 < ${p_} + popd +} + +function at-repos-atproto-service-pds-index-patch() { + f=$d/repos/atproto/services/pds/index.js + curl -sL https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/pds/index.js -o $f + d_=$d/repos/atproto + p_=$d/patching/4367-atproto-services-pds-index.diff + echo "applying patch: under ${f} for ${p_}" + pushd ${d_} + patch -p1 < ${p_} + popd +} + +function at-repos-social-app-agent-patch() { + f=$d/repos/social-app/src/state/session/agent.ts + p_=$d/patching/8980-social-app-disable-proxy.diff + d_=$d/repos/social-app + echo "applying patch: under ${f} for ${p_}" + pushd ${d_} + patch -p1 < ${p_} + popd +} + +function at-repos-social-app-disable-external-services-patch() { + f=$d/repos/social-app/src/state/geolocation/const.ts + p_=$d/patching/8980-social-app-disable-external-services.diff + d_=$d/repos/social-app + echo "applying patch: under ${f} for ${p_}" + pushd ${d_} + patch -p1 < ${p_} + popd +} + +function at-repos-atproto-service-ozone-api-patch() { + f=$d/repos/atproto/services/ozone/api.js + d_=$d/repos/atproto + p_=$d/patching/130-atproto-ozone-enable-daemon-v2.patch + echo "applying patch: under ${f} for ${p_}" + pushd ${d_} + patch -p1 < ${p_} + popd +} + +function at-repos-ozone-patch() { + #DOMAIN=syu.is + cd $d/repos + d_=$d/repos/ozone + rm -rf ${d_} + p_=$d/patching/120-ozone-runtimeEnvVars.diff + git clone https://github.com/bluesky-social/ozone + cd ${d_} + pushd ${d_} + echo "applying patch: under ${d_} for ${p_}" + patch -p1 < ${p_} + popd + + p_=$d/patching/122-ozone-enable-daemon.diff + echo "applying patch: under ${d_} for ${p_}" + pushd ${d_} + patch -p1 < ${p_} + popd + + p_=$d/patching/121-ozone-constants-fix.patch + echo "applying patch: under ${d_} for ${p_}" + pushd ${d_} + patch -p1 < ${p_} || true + popd +} + +function at-repos-build-docker-atproto() { + cd $d + docker image prune -a + if [ -z "$1" ];then + for ((i=1; i<=${#services}; i++)); do + service=${services[$i]} + docker compose build --no-cache $service + done + else + docker compose build --no-cache $1 + fi +} + +function at-repos-push-reset() { + docker restart registry + docker stop registry + docker rm registry + docker volume rm registry-data 2>/dev/null || true + docker run -d -p ${dport}:${dport} --name registry \ + --restart=always \ + -v registry-data:/var/lib/registry \ + registry:2 + sleep 3 + docker run -d -p ${dport}:${dport} --name registry --restart=always registry:2 +} + +function at-repos-push-docker() { + if [ -z "$1" ];then + for ((i=1; i<=${#services}; i++)); do + service=${services[$i]} + docker tag at-${service}:latest localhost:${dport}/${service}:latest + docker push localhost:${dport}/${service}:latest + if [ "$service" = "ozone" ];then + docker tag at-${service}:latest localhost:${dport}/${service}-web:latest + docker push localhost:${dport}/${service}-web:latest + fi + done + else + docker tag at-${1}:latest localhost:${dport}/${1}:latest + docker push localhost:${dport}/${1}:latest + fi +} + +function at-repos-pull-docker() { + cd $d + docker image prune -a + docker compose up -d --pull always +} + +at-repos-env +case "`cat /etc/hostname`" in + at) + at-repos-pull-docker + exit + ;; + *) + at-repos-push-reset + at-repos-clone + at-repos-pull + at-repos-social-app-icon + at-repos-social-app-icon-origin + at-repos-social-app-avatar-write + at-repos-social-app-agent-patch + at-repos-social-app-disable-external-services-patch + at-repos-atproto-service-bsky-api-patch + at-repos-atproto-service-pds-index-patch + at-repos-atproto-service-ozone-api-patch + at-repos-ozone-patch + if [ -n "$1" ];then + at-repos-build-docker-atproto $1 + at-repos-push-docker $1 + exit + fi + at-repos-build-docker-atproto + at-repos-push-docker + cd $d; docker compose down + ;; +esac + diff --git a/ios/.keep b/ios/.keep new file mode 100644 index 0000000..e69de29 diff --git a/ios/AppInfo.tsx b/ios/AppInfo.tsx new file mode 100644 index 0000000..6242b12 --- /dev/null +++ b/ios/AppInfo.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import {View, Text, StyleSheet, Pressable, Linking} from 'react-native' + +interface AppInfoProps { + onLinkPress?: (url: string) => void +} + +export default function AppInfo({onLinkPress}: AppInfoProps) { + const handleLinkPress = (url: string) => { + if (onLinkPress) { + onLinkPress(url) + } else { + Linking.openURL(url) + } + } + + return ( + + + About This App + + This is a customized AT Protocol social networking client. It allows you to + connect to any Personal Data Server (PDS) and participate in the decentralized + social network. + + + + + Key Features + + • Connect to any AT Protocol PDS + • Post text, images, and videos + • Follow users and view timelines + • Customize feeds and moderation settings + • Direct messaging support + + + + + Open Source + + This application is based on the Bluesky social-app, licensed under the MIT + License. The original source code is available at: + + + handleLinkPress('https://github.com/bluesky-social/social-app') + }> + github.com/bluesky-social/social-app + + + + + AT Protocol + + This app uses the AT Protocol (Authenticated Transfer Protocol), an open and + decentralized standard for social applications. + + handleLinkPress('https://atproto.com')}> + atproto.com + + + + + License + + Copyright 2023–2025 Bluesky Social PBC + + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software. + + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. + + + + + Contact + handleLinkPress('https://syu.is')}> + https://syu.is + + + + + Version 1.0.0 + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + section: { + marginBottom: 24, + }, + sectionTitle: { + fontSize: 20, + fontWeight: '600', + color: '#1d1d1f', + marginBottom: 12, + }, + paragraph: { + fontSize: 15, + lineHeight: 22, + color: '#3a3a3c', + marginBottom: 8, + }, + list: { + marginLeft: 8, + marginTop: 8, + }, + listItem: { + fontSize: 15, + lineHeight: 24, + color: '#3a3a3c', + }, + link: { + fontSize: 15, + color: '#007aff', + textDecorationLine: 'underline', + marginTop: 8, + }, + versionText: { + fontSize: 13, + color: '#8e8e93', + fontStyle: 'italic', + }, +}) diff --git a/ios/LicenseNotice.tsx b/ios/LicenseNotice.tsx new file mode 100644 index 0000000..496d367 --- /dev/null +++ b/ios/LicenseNotice.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import {View, Text, StyleSheet, Pressable, Linking} from 'react-native' + +export default function LicenseNotice() { + return ( + + Open Source Licenses + + + Bluesky Social App + MIT License + Copyright 2023–2025 Bluesky Social PBC + + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + + + Linking.openURL('https://github.com/bluesky-social/social-app') + }> + View Source Code + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 20, + color: '#1d1d1f', + }, + section: { + marginBottom: 24, + padding: 16, + backgroundColor: '#f5f5f7', + borderRadius: 8, + }, + projectName: { + fontSize: 18, + fontWeight: '600', + marginBottom: 8, + color: '#1d1d1f', + }, + license: { + fontSize: 14, + fontWeight: '500', + color: '#007aff', + marginBottom: 4, + }, + copyright: { + fontSize: 13, + color: '#3a3a3c', + marginBottom: 12, + }, + licenseText: { + fontSize: 12, + lineHeight: 18, + color: '#3a3a3c', + marginBottom: 12, + }, + link: { + fontSize: 14, + color: '#007aff', + textDecorationLine: 'underline', + marginTop: 8, + }, +}) diff --git a/ios/PrivacyContent.tsx b/ios/PrivacyContent.tsx new file mode 100644 index 0000000..d780104 --- /dev/null +++ b/ios/PrivacyContent.tsx @@ -0,0 +1,163 @@ +import React from 'react' +import {View, Text, StyleSheet, Pressable, Linking} from 'react-native' + +interface PrivacyContentProps { + onLinkPress?: (url: string) => void +} + +export default function PrivacyContent({onLinkPress}: PrivacyContentProps) { + const handleLinkPress = (url: string) => { + if (onLinkPress) { + onLinkPress(url) + } else { + Linking.openURL(url) + } + } + + return ( + + + Introduction + + This Privacy Policy explains how this AT Protocol client application + (hereinafter referred to as "the App") handles personal information. + Please read this policy carefully before using the App. + + + + + Information We Collect + + The App may collect and use the following information: + + + 1. Information Collected Automatically + + • Device information (model, OS version) + • App usage data (sessions, features used) + • Crash logs and performance data + + + 2. Information Provided by Users + + + • DID (Decentralized Identifier) and handle for authentication + + • Posts, media, and social interactions + • Profile information (avatar, display name, bio) + + + + + Important: Your data is stored on your chosen PDS (Personal Data Server). + This app does not store your content on our servers. + + + + + + How We Use Your Information + + + • To provide AT Protocol social networking features + + • To improve app performance and user experience + • To diagnose and fix technical issues + + + + + Data Sharing + + The App interacts with your chosen PDS and AppView services. Your posts and + profile information are shared according to the AT Protocol specification and + your privacy settings. + + + + + Your Rights + + You have the right to access, modify, or delete your data through your PDS. + You can also switch to a different PDS at any time while maintaining your + identity. + + + + + Contact + + For questions about this Privacy Policy, please contact: + + handleLinkPress('https://syu.is')}> + https://syu.is + + + + + Last Updated: December 3, 2025 + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + section: { + marginBottom: 24, + }, + sectionTitle: { + fontSize: 20, + fontWeight: '600', + color: '#1d1d1f', + marginBottom: 12, + }, + subTitle: { + fontSize: 16, + fontWeight: '500', + color: '#1d1d1f', + marginTop: 12, + marginBottom: 8, + }, + paragraph: { + fontSize: 15, + lineHeight: 22, + color: '#3a3a3c', + marginBottom: 8, + }, + list: { + marginLeft: 8, + marginTop: 8, + }, + listItem: { + fontSize: 15, + lineHeight: 24, + color: '#3a3a3c', + }, + highlight: { + backgroundColor: '#fff3cd', + borderLeftWidth: 4, + borderLeftColor: '#ffc107', + padding: 12, + marginTop: 12, + borderRadius: 4, + }, + highlightText: { + fontSize: 14, + lineHeight: 20, + color: '#856404', + }, + link: { + fontSize: 15, + color: '#007aff', + textDecorationLine: 'underline', + marginTop: 8, + }, + lastUpdated: { + fontSize: 13, + color: '#8e8e93', + fontStyle: 'italic', + }, +}) diff --git a/ios/PrivacyPolicy.screen.tsx b/ios/PrivacyPolicy.screen.tsx new file mode 100644 index 0000000..df5d5c8 --- /dev/null +++ b/ios/PrivacyPolicy.screen.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' + +import {usePalette} from '#/lib/hooks/usePalette' +import { + type CommonNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {s} from '#/lib/styles' +import {useSetMinimalShellMode} from '#/state/shell' +import {ScrollView} from '#/view/com/util/Views' +import * as Layout from '#/components/Layout' +import {ViewHeader} from '../com/util/ViewHeader' +import PrivacyContent from '#/components/custom/PrivacyContent' + +type Props = NativeStackScreenProps +export const PrivacyPolicyScreen = (_props: Props) => { + const pal = usePalette('default') + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) + + return ( + + + + + + + + + + ) +} diff --git a/ios/Support.screen.tsx b/ios/Support.screen.tsx new file mode 100644 index 0000000..3fddf92 --- /dev/null +++ b/ios/Support.screen.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' + +import {usePalette} from '#/lib/hooks/usePalette' +import { + type CommonNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {s} from '#/lib/styles' +import {useSetMinimalShellMode} from '#/state/shell' +import {ViewHeader} from '#/view/com/util/ViewHeader' +import {ScrollView} from '#/view/com/util/Views' +import * as Layout from '#/components/Layout' +import AppInfo from '#/components/custom/AppInfo' + +type Props = NativeStackScreenProps +export const SupportScreen = (_props: Props) => { + const pal = usePalette('default') + const setMinimalShellMode = useSetMinimalShellMode() + const {_} = useLingui() + + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) + + return ( + + + + + + + ) +} diff --git a/ios/app.config.patch.js b/ios/app.config.patch.js new file mode 100644 index 0000000..136a4d9 --- /dev/null +++ b/ios/app.config.patch.js @@ -0,0 +1,9 @@ +// Aiat app configuration overrides +module.exports = { + name: 'Aiat', + slug: 'aiat', + scheme: 'aiat', + owner: 'syui', // Your Expo account + bundleIdentifier: 'ai.syui.at', + // Icon will be set separately +} diff --git a/ios/bin/build.zsh b/ios/bin/build.zsh new file mode 100755 index 0000000..d1fe8ea --- /dev/null +++ b/ios/bin/build.zsh @@ -0,0 +1,44 @@ +#!/bin/zsh +set -e + +d=~/ai/at/repos/social-app +APP_NAME=Aiat +PKG=aiat +TEAM_NAME= +TEAM_ID= +CERT="Apple Distribution: ${TEAM_NAME} (${TEAM_ID})" +MAIL=user@example.com +KEY_CHAIN=EXAMPLE + +cd $d +# npx expo prebuild --clean +# cd ios && pod install && cd .. + +## アーカイブ +xcodebuild -workspace ios/${PKG}.xcworkspace \ + -scheme ${PKG} \ + -configuration Release \ + -archivePath build/${APP_NAME}.xcarchive \ + -allowProvisioningUpdates \ + archive + +cd build + +# IPA作成 +rm -rf Payload ${APP_NAME}.ipa +mkdir -p Payload +cp -R ${APP_NAME}.xcarchive/Products/Applications/${PKG}.app Payload/ +cp ../store.mobileprovision Payload/${PKG}.app/embedded.mobileprovision + +# entitlements抽出 +security cms -D -i Payload/${PKG}.app/embedded.mobileprovision > /tmp/profile.plist +/usr/libexec/PlistBuddy -x -c "Print :Entitlements" /tmp/profile.plist > /tmp/entitlements.plist + +codesign -f -s "$CERT" Payload/${PKG}.app/Frameworks/*.framework 2>/dev/null || true +codesign -f -s "$CERT" --entitlements /tmp/entitlements.plist Payload/${PKG}.app + +zip -r ${APP_NAME}.ipa Payload + +xcrun altool --upload-app -f ${APP_NAME}.ipa -t ios -u "${MAIL}" -p "@keychain:${KEY_CHAIN}" + +echo "Upload complete" diff --git a/ios/bin/install.zsh b/ios/bin/install.zsh new file mode 100644 index 0000000..2e372c7 --- /dev/null +++ b/ios/bin/install.zsh @@ -0,0 +1,86 @@ +#!/bin/zsh + +if [ "$1" = "social-app-custom" ];then + at-social-app-custom-pages + at-social-app-custom-screens + at-social-app-aiat-config + at-social-app-aiat-logo + at-origin-social-app + exit +fi + +function at-social-app-custom-pages() { + d_=$d/repos/social-app + custom=$d/social-app-custom + + echo "copying custom components to social-app" + + # Create components directory if not exists + mkdir -p ${d_}/src/components/custom + + # Copy custom components + cp ${custom}/PrivacyContent.tsx ${d_}/src/components/custom/ + cp ${custom}/AppInfo.tsx ${d_}/src/components/custom/ + + echo "custom components copied successfully" +} + +function at-social-app-aiat-config() { + d_=$d/repos/social-app + custom=$d/social-app-custom + + echo "applying Aiat configuration" + + # Update app.config.js + cd ${d_} + + # Backup original + cp app.config.js app.config.js.orig + + # Apply changes using sed + sed -i "s/name: 'Bluesky'/name: 'Aiat'/g" app.config.js + sed -i "s/slug: 'bluesky'/slug: 'aiat'/g" app.config.js + sed -i "s/scheme: 'bluesky'/scheme: 'aiat'/g" app.config.js + sed -i "s/owner: 'blueskysocial'/owner: 'syui'/g" app.config.js + sed -i "s/bundleIdentifier: 'xyz.blueskyweb.app'/bundleIdentifier: 'ai.syui.at'/g" app.config.js + + # Update package.json name + sed -i 's/"name": "bsky.app"/"name": "aiat"/g' package.json + + echo "Aiat configuration applied" +} + +function at-social-app-aiat-logo() { + d_=$d/repos/social-app + custom=$d/social-app-custom + + echo "applying Aiat logo" + + # Create logo directory if not exists + mkdir -p ${custom}/assets + + # Copy logo if exists in custom folder + if [ -f ${custom}/assets/icon.png ]; then + cp ${custom}/assets/icon.png ${d_}/assets/app-icons/ios_icon_default_next.png + echo "Aiat logo applied" + else + echo "Warning: Logo file not found at ${custom}/assets/icon.png" + echo "Please add your logo file there" + fi +} + +function at-social-app-custom-screens() { + d_=$d/repos/social-app + custom=$d/social-app-custom + + echo "applying custom screens" + + # Copy custom screen files + cp ${custom}/PrivacyPolicy.screen.tsx ${d_}/src/view/screens/PrivacyPolicy.tsx + cp ${custom}/Support.screen.tsx ${d_}/src/view/screens/Support.tsx + cp ${custom}/LicenseNotice.tsx ${d_}/src/components/custom/ + + echo "custom screens applied" +} + + diff --git a/patching/.keep b/patching/.keep new file mode 100644 index 0000000..e69de29 diff --git a/patching/120-ozone-runtimeEnvVars.diff b/patching/120-ozone-runtimeEnvVars.diff new file mode 100644 index 0000000..4f369a8 --- /dev/null +++ b/patching/120-ozone-runtimeEnvVars.diff @@ -0,0 +1,119 @@ +diff --git a/app/layout.tsx b/app/layout.tsx +index bfc3470..9350629 100644 +--- a/app/layout.tsx ++++ b/app/layout.tsx +@@ -5,6 +5,7 @@ import 'yet-another-react-lightbox/styles.css' + import 'yet-another-react-lightbox/plugins/thumbnails.css' + import 'yet-another-react-lightbox/plugins/captions.css' + import { ToastContainer } from 'react-toastify' ++import { PublicEnvScript } from 'next-runtime-env'; + + import { Shell } from '@/shell/Shell' + import { CommandPaletteRoot } from '@/shell/CommandPalette/Root' +@@ -36,6 +37,7 @@ export default function RootLayout({ + isDarkModeEnabled() ? 'dark' : '' + }`} + > ++ + Ozone + + ++ ++ + + blueSkyUrlMatcher.test(url) + +diff --git a/package.json b/package.json +index 8919841..750dce9 100644 +--- a/package.json ++++ b/package.json +@@ -37,6 +37,7 @@ + "kbar": "^0.1.0-beta.45", + "lande": "^1.0.10", + "next": "15.2.4", ++ "next-runtime-env": "^3.2.1", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-dropzone": "^14.3.5", diff --git a/patching/121-ozone-constants-fix.patch b/patching/121-ozone-constants-fix.patch new file mode 100644 index 0000000..42ba11d --- /dev/null +++ b/patching/121-ozone-constants-fix.patch @@ -0,0 +1,99 @@ +--- a/lib/constants.ts ++++ b/lib/constants.ts +@@ -1,29 +1,32 @@ ++import { env } from 'next-runtime-env'; ++ + export const OAUTH_SCOPE = 'atproto transition:generic transition:chat.bsky' + + export const OZONE_SERVICE_DID = +- process.env.NEXT_PUBLIC_OZONE_SERVICE_DID || undefined ++ env('NEXT_PUBLIC_OZONE_SERVICE_DID') || undefined + + export const OZONE_PUBLIC_URL = +- process.env.NEXT_PUBLIC_OZONE_PUBLIC_URL || undefined ++ env('NEXT_PUBLIC_OZONE_PUBLIC_URL') || undefined + + export const PLC_DIRECTORY_URL = +- process.env.NEXT_PUBLIC_PLC_DIRECTORY_URL || ++ env('NEXT_PUBLIC_PLC_DIRECTORY_URL') || + (process.env.NODE_ENV === 'development' + ? 'http://localhost:2582' + : 'https://plc.directory') + +-export const QUEUE_CONFIG = process.env.NEXT_PUBLIC_QUEUE_CONFIG || '{}' ++export const QUEUE_CONFIG = env('NEXT_PUBLIC_QUEUE_CONFIG') || '{}' + +-export const QUEUE_SEED = process.env.NEXT_PUBLIC_QUEUE_SEED || '' ++export const QUEUE_SEED = env('NEXT_PUBLIC_QUEUE_SEED') || '' + ++export const SOCIAL_APP_DOMAIN = env('NEXT_PUBLIC_SOCIAL_APP_DOMAIN') || 'bsky.app' + export const SOCIAL_APP_URL = +- process.env.NEXT_PUBLIC_SOCIAL_APP_URL || ++ env('NEXT_PUBLIC_SOCIAL_APP_URL') || + (process.env.NODE_ENV === 'development' + ? 'http://localhost:2584' +- : 'https://bsky.app') ++ : `https://${SOCIAL_APP_DOMAIN}`) + + export const HANDLE_RESOLVER_URL = +- process.env.NEXT_PUBLIC_HANDLE_RESOLVER_URL || ++ env('NEXT_PUBLIC_HANDLE_RESOLVER_URL') || + (process.env.NODE_ENV === 'development' + ? 'http://localhost:2584' + : 'https://api.bsky.app') +@@ -36,25 +39,25 @@ + + export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process.env + .NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS +- ? parseInt(process.env.NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS) ++ ? parseInt(env('NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS')) + : 7 + + export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process.env + .NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS +- ? parseInt(process.env.NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS) ++ ? parseInt(env('NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS')) + : 30 + + export const DOMAINS_ALLOWING_EMAIL_COMMUNICATION = ( +- process.env.NEXT_PUBLIC_DOMAINS_ALLOWING_EMAIL_COMMUNICATION || '' ++ env('NEXT_PUBLIC_DOMAINS_ALLOWING_EMAIL_COMMUNICATION') || '' + ).split(',') + + export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process.env + .NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD +- ? parseInt(process.env.NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD) ++ ? parseInt(env('NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD')) + : Infinity + + export const FALLBACK_VIDEO_URL = ( +- process.env.NEXT_PUBLIC_FALLBACK_VIDEO_URL || '' ++ env('NEXT_PUBLIC_FALLBACK_VIDEO_URL') || '' + ).split(':') + + // strike to account suspension duration mapping (in hours) +@@ -87,18 +90,18 @@ + + export const STRIKE_TO_SUSPENSION_DURATION_IN_HOURS = + parseStrikeSuspensionConfig( +- process.env.NEXT_PUBLIC_STRIKE_SUSPENSION_CONFIG || '', ++ env('NEXT_PUBLIC_STRIKE_SUSPENSION_CONFIG') || '', + ) + + export const AUTOMATED_ACTION_EMAIL_IDS = { + warningWithTakedown: +- process.env.NEXT_PUBLIC_WARNING_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID, ++ env('NEXT_PUBLIC_WARNING_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID'), + suspensionWithTakedown: +- process.env.NEXT_PUBLIC_SUSPENSION_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID, ++ env('NEXT_PUBLIC_SUSPENSION_WITH_TAKEDOWN_EMAIL_TEMPLATE_ID'), + suspensionWithoutTakedown: +- process.env.NEXT_PUBLIC_SUSPENSION_WITHOUT_TAKEDOWN_EMAIL_TEMPLATE_ID, ++ env('NEXT_PUBLIC_SUSPENSION_WITHOUT_TAKEDOWN_EMAIL_TEMPLATE_ID'), + permanentTakedown: +- process.env.NEXT_PUBLIC_PERMANENT_TAKEDOWN_EMAIL_TEMPLATE_ID, ++ env('NEXT_PUBLIC_PERMANENT_TAKEDOWN_EMAIL_TEMPLATE_ID'), + takedownWithoutStrike: +- process.env.NEXT_PUBLIC_TAKEDOWN_WITHOUT_STRIKE_EMAIL_TEMPLATE_ID, ++ env('NEXT_PUBLIC_TAKEDOWN_WITHOUT_STRIKE_EMAIL_TEMPLATE_ID'), + } diff --git a/patching/122-ozone-enable-daemon.diff b/patching/122-ozone-enable-daemon.diff new file mode 100644 index 0000000..3b783df --- /dev/null +++ b/patching/122-ozone-enable-daemon.diff @@ -0,0 +1,82 @@ +diff --git a/service/index.js b/service/index.js +index 943c281..7721cd9 100644 +--- a/service/index.js ++++ b/service/index.js +@@ -1,5 +1,7 @@ + const next = require('next') +-const { ++const ozone = require('@atproto/ozone') ++/* ++{ + readEnv, + httpLogger, + envToCfg, +@@ -7,6 +9,7 @@ const { + OzoneService, + Database, + } = require('@atproto/ozone') ++*/ + const pkg = require('@atproto/ozone/package.json') + + async function main() { +@@ -16,37 +19,48 @@ async function main() { + const frontendHandler = frontend.getRequestHandler() + await frontend.prepare() + // backend +- const env = readEnv() ++ const env = ozone.readEnv() + env.version ??= pkg.version +- const config = envToCfg(env) +- const secrets = envToSecrets(env) ++ const config = ozone.envToCfg(env) ++ const secrets = ozone.envToSecrets(env) + const migrate = process.env.OZONE_DB_MIGRATE === '1' + if (migrate) { +- const db = new Database({ ++ const db = new ozone.Database({ + url: config.db.postgresUrl, + schema: config.db.postgresSchema, + }) + await db.migrateToLatestOrThrow() + await db.close() + } +- const ozone = await OzoneService.create(config, secrets) ++ const server = await ozone.OzoneService.create(config, secrets) + // setup handlers +- ozone.app.get('/.well-known/ozone-metadata.json', (_req, res) => { ++ server.app.get('/.well-known/ozone-metadata.json', (_req, res) => { + return res.json({ +- did: ozone.ctx.cfg.service.did, +- url: ozone.ctx.cfg.service.publicUrl, +- publicKey: ozone.ctx.signingKey.did(), ++ did: server.ctx.cfg.service.did, ++ url: server.ctx.cfg.service.publicUrl, ++ publicKey: server.ctx.signingKey.did(), + }) + }) + // Note: We must use `use()` here. This should be the last middleware. +- ozone.app.use((req, res) => { ++ server.app.use((req, res) => { + void frontendHandler(req, res, undefined) + }) + // run +- const httpServer = await ozone.start() ++ const httpServer = await server.start() ++ // starts: involve ops from atproto/packages/dev-env/src/ozone.ts >>> ++ ozone.httpLogger.info('starts ozone daemon') ++ const daemon = await ozone.OzoneDaemon.create(config, secrets) ++ await daemon.start() ++ //if (process.env.OZONE_ENABLE_EVENT_REVERSER != 'true') // atproto/services/ozone/daemon.js doesn't stop eventReverser ++ //{ ++ // ozone.httpLogger.info('disable ozone daemon eventReverser') ++ // await daemon.ctx.eventReverser.destroy() ++ //} ++ // ends: involve ops from atproto/packages/dev-env/src/ozone.ts <<< ++ + /** @type {import('net').AddressInfo} */ + const addr = httpServer.address() +- httpLogger.info(`Ozone is running at http://localhost:${addr.port}`) ++ ozone.httpLogger.info(`Ozone is running at http://localhost:${addr.port}`) + } + + main().catch(console.error) diff --git a/patching/130-atproto-ozone-enable-daemon-v2.patch b/patching/130-atproto-ozone-enable-daemon-v2.patch new file mode 100644 index 0000000..792c655 --- /dev/null +++ b/patching/130-atproto-ozone-enable-daemon-v2.patch @@ -0,0 +1,28 @@ +--- a/services/ozone/api.js ++++ b/services/ozone/api.js +@@ -23,6 +23,7 @@ + Database, + OzoneService, + envToCfg, ++ OzoneDaemon, + envToSecrets, + httpLogger, + readEnv, +@@ -79,10 +80,17 @@ + + httpLogger.info('ozone is running') + ++ // Start OzoneDaemon for label events ++ httpLogger.info('starting ozone daemon') ++ const daemon = await OzoneDaemon.create(cfg, secrets) ++ await daemon.start() ++ httpLogger.info('ozone daemon is running') ++ + // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) + process.on('SIGTERM', async () => { + httpLogger.info('ozone is stopping') + ++ await daemon.destroy() + await ozone.destroy() + + httpLogger.info('ozone is stopped') diff --git a/patching/130-atproto-ozone-enable-daemon.patch b/patching/130-atproto-ozone-enable-daemon.patch new file mode 100644 index 0000000..7a10d3d --- /dev/null +++ b/patching/130-atproto-ozone-enable-daemon.patch @@ -0,0 +1,33 @@ +--- a/services/ozone/api.js ++++ b/services/ozone/api.js +@@ -20,6 +20,7 @@ const { + MultiImageInvalidator, + } = require('@atproto/aws') + const { + Database, + OzoneService, ++ OzoneDaemon, + envToCfg, + envToSecrets, + httpLogger, +@@ -76,10 +77,17 @@ const main = async () => { + const ozone = await OzoneService.create(cfg, secrets, { imgInvalidator }) + + await ozone.start() + + httpLogger.info('ozone is running') + ++ // Start OzoneDaemon for label events ++ httpLogger.info('starting ozone daemon') ++ const daemon = await OzoneDaemon.create(cfg, secrets) ++ await daemon.start() ++ httpLogger.info('ozone daemon is running') ++ + // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) + process.on('SIGTERM', async () => { + httpLogger.info('ozone is stopping') + ++ await daemon.destroy() + await ozone.destroy() + + httpLogger.info('ozone is stopped') diff --git a/patching/4367-atproto-services-bsky-api.diff b/patching/4367-atproto-services-bsky-api.diff new file mode 100644 index 0000000..f887230 --- /dev/null +++ b/patching/4367-atproto-services-bsky-api.diff @@ -0,0 +1,160 @@ +--- a/services/bsky/api.js 2025-12-03 11:04:54 ++++ b/services/bsky/api.js 2025-12-03 11:00:02 +@@ -1,62 +1,105 @@ + /* eslint-env node */ + /* eslint-disable import/order */ +- ++// https://github.com/bluesky-social/atproto/blob/main/services/bsky/api.js + 'use strict' + +-const dd = require('dd-trace') ++//const dd = require('dd-trace') ++// ++//dd.tracer ++// .init() ++// .use('http2', { ++// client: true, // calls into dataplane ++// server: false, ++// }) ++// .use('express', { ++// hooks: { ++// request: (span, req) => { ++// maintainXrpcResource(span, req) ++// }, ++// }, ++// }) + +-dd.tracer +- .init() +- .use('http2', { +- client: true, // calls into dataplane +- server: false, +- }) +- .use('express', { +- hooks: { +- request: (span, req) => { +- maintainXrpcResource(span, req) +- }, +- }, +- }) +- + // modify tracer in order to track calls to dataplane as a service with proper resource names + const DATAPLANE_PREFIX = '/bsky.Service/' +-const origStartSpan = dd.tracer._tracer.startSpan +-dd.tracer._tracer.startSpan = function (name, options) { +- if ( +- name !== 'http.request' || +- options?.tags?.component !== 'http2' || +- !options?.tags?.['http.url'] +- ) { +- return origStartSpan.call(this, name, options) +- } +- const uri = new URL(options.tags['http.url']) +- if (!uri.pathname.startsWith(DATAPLANE_PREFIX)) { +- return origStartSpan.call(this, name, options) +- } +- options.tags['service.name'] = 'dataplane-bsky' +- options.tags['resource.name'] = uri.pathname.slice(DATAPLANE_PREFIX.length) +- return origStartSpan.call(this, name, options) +-} ++//const origStartSpan = dd.tracer._tracer.startSpan ++//dd.tracer._tracer.startSpan = function (name, options) { ++// if ( ++// name !== 'http.request' || ++// options?.tags?.component !== 'http2' || ++// !options?.tags?.['http.url'] ++// ) { ++// return origStartSpan.call(this, name, options) ++// } ++// const uri = new URL(options.tags['http.url']) ++// if (!uri.pathname.startsWith(DATAPLANE_PREFIX)) { ++// return origStartSpan.call(this, name, options) ++// } ++// options.tags['service.name'] = 'dataplane-bsky' ++// options.tags['resource.name'] = uri.pathname.slice(DATAPLANE_PREFIX.length) ++// return origStartSpan.call(this, name, options) ++//} + + // Tracer code above must come before anything else + const assert = require('node:assert') + const cluster = require('node:cluster') + const path = require('node:path') + +-const { BskyAppView, ServerConfig } = require('@atproto/bsky') +-const { Secp256k1Keypair } = require('@atproto/crypto') ++const bsky = require('/app/packages/bsky') // import all bsky features ++const { Secp256k1Keypair } = require('/app/packages/crypto') + + const main = async () => { + const env = getEnv() +- const config = ServerConfig.readEnv() ++ const config = bsky.ServerConfig.readEnv() + assert(env.serviceSigningKey, 'must set BSKY_SERVICE_SIGNING_KEY') + const signingKey = await Secp256k1Keypair.import(env.serviceSigningKey) +- const bsky = BskyAppView.create({ config, signingKey }) +- await bsky.start() ++ ++// starts: involve logics in packages/dev-env/src/bsky.ts >>>>>>>>>>>>> ++// Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." ++ const migrationDb = new bsky.Database({ ++ url: env.dbPostgresUrl, ++ schema: env.dbPostgresSchema, ++ }) ++ if (env.migration) { ++ await migrationDb.migrateToOrThrow(env.migration) ++ } else { ++ await migrationDb.migrateToLatestOrThrow() ++ } ++ await migrationDb.close() ++ ++ const db = new bsky.Database({ ++ url: env.dbPostgresUrl, ++ schema: env.dbPostgresSchema, ++ poolSize: 10, ++ }) ++ ++ const dataplane = await bsky.DataPlaneServer.create( ++ db, ++ env.dataplanePort, ++ config.didPlcUrl ++ ) ++ ++ const bsync = await bsky.MockBsync.create(db, env.bsyncPort) ++ ++// ends: involve logics in packages/dev-env/src/bsky.ts <<<<<<<<<<<<< ++ ++ const server = bsky.BskyAppView.create({ config, signingKey }) ++// starts: involve logics in packages/dev-env/src/bsky.ts >>>>>>>>>>>>> ++ const sub = new bsky.RepoSubscription({ ++ service: env.repoProvider, ++ db, ++ idResolver: dataplane.idResolver, ++ background: new bsky.BackgroundQueue(db), ++ }) ++// ends: involve logics in packages/dev-env/src/bsky.ts <<<<<<<<<<<<< ++ await server.start() ++ sub.start() // involve logics in packages/dev-env/src/bsky.ts + // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) + const shutdown = async () => { +- await bsky.destroy() ++ await server.destroy() ++ await bsync.destroy() ++ await dataplane.destroy() ++ await sub.destroy() ++ await db.close() + } + process.on('SIGTERM', shutdown) + process.on('disconnect', shutdown) // when clustering +@@ -64,6 +107,12 @@ + + const getEnv = () => ({ + serviceSigningKey: process.env.BSKY_SERVICE_SIGNING_KEY || undefined, ++ dbPostgresUrl: process.env.BSKY_DB_POSTGRES_URL || undefined, ++ dbPostgresSchema: process.env.BSKY_DB_POSTGRES_SCHEMA || undefined, ++ dataplanePort : maybeParseInt(process.env.BSKY_DATAPLANE_PORT) || undefined, ++ bsyncPort : maybeParseInt(process.env.BSKY_BSYNC_PORT) || undefined, ++ migration: process.env.ENABLE_MIGRATIONS === 'true' || undefined, ++ repoProvider: process.env.BSKY_REPO_PROVIDER || undefined + }) + + const maybeParseInt = (str) => { diff --git a/patching/4367-atproto-services-pds-index.diff b/patching/4367-atproto-services-pds-index.diff new file mode 100644 index 0000000..a8165f8 --- /dev/null +++ b/patching/4367-atproto-services-pds-index.diff @@ -0,0 +1,20 @@ +--- a/services/pds/index.js 2025-12-03 11:04:54 ++++ b/services/pds/index.js 2025-12-02 22:11:39 +@@ -1,5 +1,5 @@ + /* eslint-env node */ +- ++// https://github.com/bluesky-social/atproto/blob/main/services/pds/index.js + 'use strict' + + const { +@@ -8,8 +8,8 @@ + envToSecrets, + httpLogger, + readEnv, +-} = require('@atproto/pds') +-const pkg = require('@atproto/pds/package.json') ++} = require('/app/packages/pds') ++const pkg = require('/app/packages/pds/package.json') + + const main = async () => { + const env = readEnv() diff --git a/patching/8980-social-app-disable-external-services.diff b/patching/8980-social-app-disable-external-services.diff new file mode 100644 index 0000000..74ffc22 --- /dev/null +++ b/patching/8980-social-app-disable-external-services.diff @@ -0,0 +1,17 @@ +--- a/src/state/geolocation/const.ts ++++ b/src/state/geolocation/const.ts +@@ -3,9 +3,10 @@ import {BAPP_CONFIG_DEV_URL, IS_DEV} from '#/env' + import {type Device} from '#/storage' + + export const IPCC_URL = `https://bsky.app/ipcc` +-export const BAPP_CONFIG_URL_PROD = `https://ip.bsky.app/config` +-export const BAPP_CONFIG_URL = IS_DEV +- ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_URL_PROD) +- : BAPP_CONFIG_URL_PROD ++// Disabled for self-hosted environment to avoid CORS errors ++// export const BAPP_CONFIG_URL_PROD = `https://ip.bsky.app/config` ++// export const BAPP_CONFIG_URL = IS_DEV ++// ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_URL_PROD) ++// : BAPP_CONFIG_URL_PROD ++export const BAPP_CONFIG_URL = null + export const GEOLOCATION_CONFIG_URL = BAPP_CONFIG_URL diff --git a/patching/8980-social-app-disable-proxy.diff b/patching/8980-social-app-disable-proxy.diff new file mode 100644 index 0000000..4bc454f --- /dev/null +++ b/patching/8980-social-app-disable-proxy.diff @@ -0,0 +1,44 @@ +diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts +index 36d19299b..ba095436a 100644 +--- a/src/state/session/agent.ts ++++ b/src/state/session/agent.ts +@@ -39,7 +39,8 @@ 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()) ++ // Disable proxy for self-hosted environments ++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + return agent + } + +@@ -77,7 +78,8 @@ export async function createAgentAndResume( + } + } + +- agent.configureProxy(BLUESKY_PROXY_HEADER.get()) ++ // Disable proxy for self-hosted environments ++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + + return agent.prepare(gates, moderation, onSessionChange) + } +@@ -112,7 +114,8 @@ export async function createAgentAndLogin( + const gates = tryFetchGates(account.did, 'prefer-fresh-gates') + const moderation = configureModerationForAccount(agent, account) + +- agent.configureProxy(BLUESKY_PROXY_HEADER.get()) ++ // Disable proxy for self-hosted environments ++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + + return agent.prepare(gates, moderation, onSessionChange) + } +@@ -201,7 +204,8 @@ export async function createAgentAndCreateAccount( + logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`}) + } + +- agent.configureProxy(BLUESKY_PROXY_HEADER.get()) ++ // Disable proxy for self-hosted environments ++ // agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + + return agent.prepare(gates, moderation, onSessionChange) + }