#!/bin/zsh function at-repos-env() { APP_PASSWORD=xxx host=syu.is handle=ai.syui.ai did=did:plc:6qyecktefllvenje24fcxnie 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" "feed" ) handles=( "syui.syui.ai" "ai.syui.ai" "apple.syu.is" ) d=${0:a:h} dh=${0:a:h:h} name=${host%%.*} domain=${host##*.} dport=5000 } # Arrays for patch management typeset -a FAILED_PATCHES # Patch file lists typeset -a PATCH_FILES_CURL PATCH_FILES_CURL=( "4367-atproto-services-bsky-api.diff:https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/bsky/api.js:services/bsky/api.js" "4367-atproto-services-pds-index.diff:https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/pds/index.js:services/pds/index.js" ) typeset -a PATCH_FILES PATCH_FILES=( "170-pds-oauth-same-site-fix.patch" "8980-social-app-disable-proxy.diff" "disable-statsig-sdk.diff" "140-social-app-yarn-network-timeout.patch" "130-atproto-ozone-enable-daemon-v2.patch" "190-bgs-disable-ratelimit.patch" "200-feed-generator-custom.patch" ) 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 -u 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-avatar-write() { did_admin=did:plc:6qyecktefllvenje24fcxnie dt=$d/repos/social-app/src cd $dt grep -R syu.is .|cut -d : -f 1|sort -u|xargs sed -i "s/syu.is/${host}/g" grep -R web.syu.is .|cut -d : -f 1|sort -u|xargs sed -i "s/web.syu.is/web.${host}/g" 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 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" } # Common patch function with status detection function apply-patch() { local patch_name=$1 local target_dir=$2 local patch_file=$3 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📝 Patch: ${patch_name}" echo " Target: ${target_dir}" echo " File: ${patch_file}" pushd ${target_dir} > /dev/null # Check if patch is already applied (reverse dry-run succeeds) if patch --dry-run -p1 -R < ${patch_file} > /dev/null 2>&1; then echo "✅ Already applied - skipping" popd > /dev/null echo "" return 0 fi # Check if patch can be applied (forward dry-run succeeds) if patch --dry-run -p1 < ${patch_file} > /dev/null 2>&1; then echo "🔧 Applying patch..." if patch -p1 < ${patch_file}; then echo "✅ Applied successfully" popd > /dev/null echo "" return 0 else echo "❌ Failed to apply" FAILED_PATCHES+=("${patch_name} (${patch_file})") popd > /dev/null echo "" return 1 fi else echo "⚠️ Cannot apply - file may have been modified" echo " Please check manually" FAILED_PATCHES+=("${patch_name} (${patch_file}) - file modified") popd > /dev/null echo "" return 1 fi } # Function to display failed patches summary function show-failed-patches() { if [ ${#FAILED_PATCHES[@]} -eq 0 ]; then echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "✅ All patches applied successfully!" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" return 0 fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "⚠️ FAILED PATCHES SUMMARY" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "The following patches could not be applied:" echo "" for failed_patch in "${FAILED_PATCHES[@]}"; do echo " ❌ ${failed_patch}" done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" } # Helper function for applying patches function patch-apply() { local name=$1 local target=$2 local patch_file=$3 apply-patch "${name}" "$d/repos/${target}" "$d/patching/${patch_file}" } # Helper function for patches with curl download function patch-apply-with-curl() { local name=$1 local target=$2 local patch_file=$3 local download_url=$4 local download_target=$5 curl -sL "${download_url}" -o "$d/repos/${target}/${download_target}" apply-patch "${name}" "$d/repos/${target}" "$d/patching/${patch_file}" } # Auto-apply patches from list function at-repos-patch-apply-all() { # Apply curl patches for patch_info in "${PATCH_FILES_CURL[@]}"; do local filename="${patch_info%%:*}" local rest="${patch_info#*:}" local download_url="${rest%%:*}" local download_target="${rest#*:}" local title="${filename%.*}" local repo="" # Determine repo from filename if [[ $filename == *"atproto"* ]]; then repo="atproto" elif [[ $filename == *"pds"* ]]; then repo="atproto" fi patch-apply-with-curl "$title" "$repo" "$filename" "$download_url" "$download_target" done # Apply regular patches for filename in "${PATCH_FILES[@]}"; do local title="${filename%.*}" local repo="" # Determine repo from filename if [[ $filename == *"social-app"* || $filename == *"statsig"* ]]; then repo="social-app" elif [[ $filename == *"atproto"* ]]; then repo="atproto" elif [[ $filename == *"pds"* ]]; then repo="atproto" elif [[ $filename == *"indigo"* || $filename == *"bgs"* ]]; then repo="indigo" elif [[ $filename == *"feed"* ]]; then repo="feed-generator" fi patch-apply "$title" "$repo" "$filename" done } function at-repos-ozone-patch() { cd $d/repos d_=$d/repos/ozone rm -rf ${d_} git clone https://github.com/bluesky-social/ozone apply-patch "Ozone enable daemon" "${d_}" "$d/patching/122-ozone-enable-daemon.diff" if [ -f "$d/patching/150-ozone-plc-fix.patch" ]; then apply-patch "Ozone plc fix" "${d_}" "$d/patching/150-ozone-plc-fix.patch" fi if [ -f "$d/patching/160-ozone-oauth-redirect-fix.patch" ]; then apply-patch "Ozone oauth redirect fix" "${d_}" "$d/patching/160-ozone-oauth-redirect-fix.patch" fi # Apply constants fix and do additional sed replacements pushd ${d_} > /dev/null if [ -f "$d/patching/121-ozone-constants-fix.patch" ]; then patch -p1 < "$d/patching/121-ozone-constants-fix.patch" 2>/dev/null || true fi # Replace process.env with env() sed -i 's/process\.env\.\(NEXT_PUBLIC_[A-Z_]*\)/env('\''\1'\'')/g' lib/constants.ts 2>/dev/null || true sed -i 's/process\.env\.NODE_ENV/env('\''NODE_ENV'\'')/g' lib/constants.ts 2>/dev/null || true # Add missing SOCIAL_APP_DOMAIN constant after SOCIAL_APP_URL sed -i '/^export const SOCIAL_APP_URL =/,/^$/{ /^$/a\ export const SOCIAL_APP_DOMAIN =\ env('\''NEXT_PUBLIC_SOCIAL_APP_DOMAIN'\'') || '\''bsky.app'\''\ }' lib/constants.ts 2>/dev/null || true # Fix multiline process.env patterns sed -i '/^export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^ : 7$/ { s/^export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = env('\''NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'\'')/ /^ \.NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d }' lib/constants.ts 2>/dev/null || true sed -i '/^export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^ : 30$/ { s/^export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = env('\''NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'\'')/ /^ \.NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d }' lib/constants.ts 2>/dev/null || true sed -i '/^export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process\.env$/,/^ : Infinity$/ { s/^export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process\.env$/export const HIGH_PROFILE_FOLLOWER_THRESHOLD = env('\''NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD'\'')/ /^ \.NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD$/d }' lib/constants.ts 2>/dev/null || true # Fix parseInt() to handle undefined by adding || '' sed -i "s/parseInt(env('\([^']*\)'))/parseInt(env('\1') || '0')/g" lib/constants.ts 2>/dev/null || true popd > /dev/null } 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() { if [ -n "$(docker ps -q -f name=registry)" ]; then echo "Registry is already running." docker restart registry docker stop registry docker rm registry docker volume rm registry-data 2>/dev/null || true fi docker run -d -p ${dport}:${dport} --name registry \ --restart=always \ -v registry-data:/var/lib/registry \ 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}-web: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 } function at-repos-reset-bgs-db() { dp=at-database-1 BGS_ADMIN_KEY=`cat $d/envs/bgs | grep BGS_ADMIN_KEY | cut -d '=' -f 2` echo "🛑 Stopping BGS..." docker compose stop bgs echo "🗑️ Cleaning data..." sudo rm -rf $d/data/bgs/* echo "♻️ Resetting Databases..." docker exec -i $dp psql -U postgres -c "DROP DATABASE IF EXISTS bgs;" docker exec -i $dp psql -U postgres -c "CREATE DATABASE bgs;" docker exec -i $dp psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE bgs TO postgres;" docker exec -i $dp psql -U postgres -c "DROP DATABASE IF EXISTS carstore;" docker exec -i $dp psql -U postgres -c "CREATE DATABASE carstore;" docker exec -i $dp psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE carstore TO postgres;" echo "🚀 Starting BGS to initialize tables..." docker compose up -d bgs echo "⏳ Waiting 10s for BGS migration..." sleep 10 echo "⚙️ Updating Slurp Config..." docker exec -i $dp psql -U postgres -d bgs -c "UPDATE slurp_configs SET new_subs_disabled = false, new_pds_per_day_limit = 1000 WHERE id = 1;" echo "🔗 Registering Trusted Domain & Resetting Repos..." # Retry loop for addTrustedDomain as BGS might still be warming up for i in {1..5}; do if curl -f -X POST "https://bgs.${host}/admin/pds/addTrustedDomain?domain=${host}" -H "Authorization: Bearer ${BGS_ADMIN_KEY}"; then echo "✅ Trusted domain registered" break fi echo "Bot failed to contact BGS (attempt $i/5)... waiting 5s" sleep 5 done for ((i=1; i<=${#handles}; i++)); do handle=${handles[$i]} did=`curl -sL "https://${host}/xrpc/com.atproto.repo.describeRepo?repo=${handle}" |jq -r .did` if [ ! -z "$did" ] && [ "$did" != "null" ]; then echo "Resetting repo: $handle ($did)" curl -X POST "https://bgs.${host}/admin/repo/reset?did=${did}" \ -H "Authorization: Bearer ${BGS_ADMIN_KEY}" else echo "Skipping reset for $handle (DID not found)" fi done } function at-repos-feed-generator-start-push() { cd $d/repos/feed-generator yarn install FEEDGEN_HANDLE=${handle} FEEDGEN_PASSWORD=${APP_PASSWORD} FEEDGEN_RECORD_NAME=app FEEDGEN_AVATAR=$d/repos/atproto/packages/dev-env/assets/at.png npx tsx scripts/publish.ts } function at-repos-feed-generator-update() { resp=$(curl -sL -X POST -H "Content-Type: application/json" -d "{\"identifier\":\"$handle\",\"password\":\"${APP_PASSWORD}\"}" https://${host}/xrpc/com.atproto.server.createSession) token=$(echo $resp | jq -r .accessJwt) if [ -z "$token" ] || [ "$token" == "null" ]; then echo "Login failed: $resp" exit 1 fi avatar_json="{\"\$type\":\"blob\",\"ref\":{\"\$link\":\"${img_id}\"},\"mimeType\":\"image/jpeg\",\"size\":375259}" # 3. Delete cmd record #curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $token" \ # -d "{\"repo\":\"$handle\",\"collection\":\"app.bsky.feed.generator\",\"rkey\":\"cmd\"}" \ # https://${host}/xrpc/com.atproto.repo.deleteRecord # 4. Put app record echo "Creating app record..." now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") # Create JSON payload # Note: feeding avatar_json directly into jq payload=$(jq -n \ --arg repo "$handle" \ --arg collection "app.bsky.feed.generator" \ --arg rkey "app" \ --arg did "did:web:feed.${host}" \ --arg type "app.bsky.feed.generator" \ --arg created "$now" \ --arg display "App Feed" \ --arg desc "Automated App Feed" \ --argjson avatar "$avatar_json" \ '{ repo: $repo, collection: $collection, rkey: $rkey, record: { did: $did, "$type": $type, createdAt: $created, displayName: $display, description: $desc, avatar: $avatar } }') curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $token" \ -d @- \ https://${host}/xrpc/com.atproto.repo.putRecord } at-repos-env case "$1" in pull) at-repos-clone at-repos-pull exit ;; patch) at-repos-social-app-avatar-write at-repos-patch-apply-all at-repos-ozone-patch show-failed-patches exit ;; build) at-repos-build-docker-atproto $2 exit ;; push) at-repos-push-docker $2 exit ;; reset) at-repos-push-reset exit ;; down) cd $d;docker compose down exit ;; feed-push) at-repos-feed-generator-start-push exit ;; esac case "`cat /etc/hostname`" in at) if [ "$1" = "bgs-reset" ];then at-repos-reset-bgs-db exit fi at-repos-pull-docker exit ;; *) at-repos-clone at-repos-pull at-repos-social-app-avatar-write at-repos-patch-apply-all at-repos-ozone-patch show-failed-patches at-repos-build-docker-atproto at-repos-push-docker cd $d; docker compose down ;; esac