A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
11
fork

Configure Feed

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

at main 627 lines 15 kB view raw
1#!/usr/bin/env bash 2 3set -euo pipefail 4 5APP_NAME="tweets-2-bsky" 6LEGACY_APP_NAME="twitter-mirror" 7SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 8cd "$SCRIPT_DIR" 9 10RUNTIME_DIR="$SCRIPT_DIR/data/runtime" 11PID_FILE="$RUNTIME_DIR/${APP_NAME}.pid" 12LOCK_DIR="$RUNTIME_DIR/.update.lock" 13 14CONFIG_FILE="$SCRIPT_DIR/config.json" 15ENV_FILE="$SCRIPT_DIR/.env" 16 17DO_INSTALL=1 18DO_BUILD=1 19DO_NATIVE_REBUILD=1 20DO_RESTART=1 21REMOTE_OVERRIDE="" 22BRANCH_OVERRIDE="" 23ORIGINAL_ARGS=("$@") 24UPDATE_SH_REEXECED="${UPDATE_SH_REEXECED:-0}" 25 26STASH_REF="" 27STASH_CREATED=0 28STASH_RESTORED=0 29UNTRACKED_COUNT=0 30 31BACKUP_SOURCES=() 32BACKUP_PATHS=() 33BUN_BIN="" 34ORIGINAL_SCRIPT_CHECKSUM="" 35 36usage() { 37 cat <<'USAGE' 38Usage: ./update.sh [options] 39 40Default behavior: 41 - Pull latest git changes safely 42 - Install dependencies 43 - Rebuild native modules if needed 44 - Build server + web dashboard 45 - Restart existing runtime (PM2 or nohup) when possible 46 47Options: 48 --remote <name> Git remote to pull from (default: origin or first remote) 49 --branch <name> Git branch to pull (default: current branch or remote HEAD) 50 --skip-install Skip bun install 51 --skip-build Skip bun run build 52 --skip-native-rebuild Skip native-module rebuild checks 53 --no-restart Do not restart process after update 54 -h, --help Show this help 55USAGE 56} 57 58require_command() { 59 local command_name="$1" 60 if ! command -v "$command_name" >/dev/null 2>&1; then 61 echo "❌ Required command not found: $command_name" 62 exit 1 63 fi 64} 65 66ensure_bun_runtime() { 67 install_latest_bun() { 68 if command -v curl >/dev/null 2>&1; then 69 curl -fsSL https://bun.sh/install | bash >/dev/null 70 return 0 71 fi 72 if command -v wget >/dev/null 2>&1; then 73 wget -qO- https://bun.sh/install | bash >/dev/null 74 return 0 75 fi 76 77 echo "❌ Bun is required, and curl/wget is unavailable for auto-install." 78 echo " Install Bun manually: https://bun.com/docs/installation" 79 exit 1 80 } 81 82 resolve_bun_bin() { 83 if command -v bun >/dev/null 2>&1; then 84 command -v bun 85 return 0 86 fi 87 if [[ -x "${HOME}/.bun/bin/bun" ]]; then 88 printf '%s\n' "${HOME}/.bun/bin/bun" 89 return 0 90 fi 91 return 1 92 } 93 94 if ! BUN_BIN="$(resolve_bun_bin)"; then 95 echo "📦 Bun not found. Installing latest Bun..." 96 install_latest_bun 97 BUN_BIN="$(resolve_bun_bin || true)" 98 fi 99 100 if [[ -z "$BUN_BIN" || ! -x "$BUN_BIN" ]]; then 101 echo "❌ Bun could not be resolved." 102 echo " Install Bun manually: https://bun.com/docs/installation" 103 exit 1 104 fi 105 106 export PATH="$(dirname "$BUN_BIN"):$PATH" 107 108 if ! "$BUN_BIN" upgrade >/dev/null 2>&1; then 109 echo "⚠️ Bun auto-upgrade failed. Reinstalling latest Bun..." 110 install_latest_bun 111 BUN_BIN="$(resolve_bun_bin || true)" 112 fi 113 114 if [[ -z "$BUN_BIN" || ! -x "$BUN_BIN" ]]; then 115 echo "❌ Bun could not be resolved after auto-upgrade." 116 echo " Install Bun manually: https://bun.com/docs/installation" 117 exit 1 118 fi 119 120 export PATH="$(dirname "$BUN_BIN"):$PATH" 121 122 local bun_major 123 bun_major="$($BUN_BIN --version | awk -F. '{print $1}' 2>/dev/null || echo 0)" 124 if [[ "$bun_major" -lt 1 ]]; then 125 echo "❌ Bun 1.x+ is required. Current: $($BUN_BIN --version 2>/dev/null || echo 'unknown')" 126 exit 1 127 fi 128} 129 130run_bun() { 131 "$BUN_BIN" "$@" 132} 133 134compute_file_checksum() { 135 local file="$1" 136 137 if command -v sha256sum >/dev/null 2>&1; then 138 sha256sum "$file" | awk '{print $1}' 139 return 0 140 fi 141 142 if command -v shasum >/dev/null 2>&1; then 143 shasum -a 256 "$file" | awk '{print $1}' 144 return 0 145 fi 146 147 if command -v openssl >/dev/null 2>&1; then 148 openssl dgst -sha256 "$file" | awk '{print $NF}' 149 return 0 150 fi 151 152 return 1 153} 154 155acquire_lock() { 156 mkdir -p "$RUNTIME_DIR" 157 if ! mkdir "$LOCK_DIR" 2>/dev/null; then 158 echo "❌ Another update appears to be running." 159 echo " If this is stale, remove: $LOCK_DIR" 160 exit 1 161 fi 162} 163 164release_lock() { 165 rmdir "$LOCK_DIR" >/dev/null 2>&1 || true 166} 167 168backup_file() { 169 local file="$1" 170 if [[ ! -f "$file" ]]; then 171 return 0 172 fi 173 174 local base 175 base="$(basename "$file")" 176 local backup_path 177 backup_path="$(mktemp_file "tweets2bsky-${base}")" 178 cp "$file" "$backup_path" 179 BACKUP_SOURCES+=("$file") 180 BACKUP_PATHS+=("$backup_path") 181} 182 183restore_backups() { 184 local idx 185 for idx in "${!BACKUP_SOURCES[@]}"; do 186 local src="${BACKUP_SOURCES[$idx]}" 187 local bak="${BACKUP_PATHS[$idx]}" 188 if [[ -f "$bak" ]]; then 189 cp "$bak" "$src" 190 rm -f "$bak" 191 fi 192 done 193} 194 195cleanup() { 196 restore_backups 197 release_lock 198} 199 200mktemp_file() { 201 local prefix="$1" 202 203 if mktemp --version >/dev/null 2>&1; then 204 mktemp "${TMPDIR:-/tmp}/${prefix}.XXXXXX" 205 return 0 206 fi 207 208 local tmp_root 209 tmp_root="${TMPDIR:-/tmp}" 210 mktemp -t "${prefix}.XXXXXX" 2>/dev/null || mktemp "${tmp_root}/${prefix}.XXXXXX" 211} 212 213ensure_git_repo() { 214 if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then 215 echo "❌ This directory is not a git repository: $SCRIPT_DIR" 216 exit 1 217 fi 218} 219 220resolve_remote() { 221 if [[ -n "$REMOTE_OVERRIDE" ]]; then 222 if ! git remote | grep -qx "$REMOTE_OVERRIDE"; then 223 echo "❌ Remote '$REMOTE_OVERRIDE' does not exist." 224 exit 1 225 fi 226 printf '%s\n' "$REMOTE_OVERRIDE" 227 return 0 228 fi 229 230 if git remote | grep -qx "origin"; then 231 printf '%s\n' "origin" 232 return 0 233 fi 234 235 local first_remote 236 first_remote="$(git remote | head -n 1)" 237 if [[ -z "$first_remote" ]]; then 238 echo "❌ No git remote configured." 239 exit 1 240 fi 241 242 printf '%s\n' "$first_remote" 243} 244 245resolve_branch() { 246 local remote="$1" 247 248 if [[ -n "$BRANCH_OVERRIDE" ]]; then 249 printf '%s\n' "$BRANCH_OVERRIDE" 250 return 0 251 fi 252 253 local current_branch 254 current_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" 255 if [[ -n "$current_branch" ]] && git show-ref --verify --quiet "refs/remotes/${remote}/${current_branch}"; then 256 printf '%s\n' "$current_branch" 257 return 0 258 fi 259 260 local remote_head 261 remote_head="$(git symbolic-ref --quiet --short "refs/remotes/${remote}/HEAD" 2>/dev/null || true)" 262 if [[ -n "$remote_head" ]]; then 263 printf '%s\n' "${remote_head#${remote}/}" 264 return 0 265 fi 266 267 if git show-ref --verify --quiet "refs/remotes/${remote}/main"; then 268 printf '%s\n' "main" 269 return 0 270 fi 271 272 if git show-ref --verify --quiet "refs/remotes/${remote}/master"; then 273 printf '%s\n' "master" 274 return 0 275 fi 276 277 if [[ -n "$current_branch" ]]; then 278 printf '%s\n' "$current_branch" 279 return 0 280 fi 281 282 echo "❌ Could not determine target branch for remote '$remote'." 283 exit 1 284} 285 286tracked_tree_dirty() { 287 [[ -n "$(git status --porcelain --untracked-files=no)" ]] 288} 289 290stash_local_changes() { 291 if ! tracked_tree_dirty; then 292 return 0 293 fi 294 295 echo "🧳 Stashing local changes before update..." 296 297 local before after message 298 before="$(git stash list -n 1 --format=%gd || true)" 299 message="tweets-2-bsky-update-autostash-$(date -u +%Y%m%d-%H%M%S)" 300 git stash push -m "$message" >/dev/null 301 after="$(git stash list -n 1 --format=%gd || true)" 302 303 if [[ -n "$after" && "$after" != "$before" ]]; then 304 STASH_REF="$after" 305 STASH_CREATED=1 306 echo "✅ Saved local changes to $STASH_REF" 307 fi 308} 309 310print_untracked_notice() { 311 local count 312 count="$(git ls-files --others --exclude-standard | wc -l | tr -d '[:space:]')" 313 UNTRACKED_COUNT="$count" 314 315 if [[ "$count" -gt 0 ]]; then 316 echo "ℹ️ Leaving $count untracked file(s) untouched (not stashed)." 317 echo " This avoids slow/hanging updates on large data directories." 318 fi 319} 320 321restore_stash_if_needed() { 322 if [[ "$STASH_CREATED" -ne 1 || -z "$STASH_REF" ]]; then 323 return 0 324 fi 325 326 echo "🔁 Restoring stashed local changes ($STASH_REF)..." 327 if git stash apply --index "$STASH_REF" >/dev/null 2>&1; then 328 git stash drop "$STASH_REF" >/dev/null 2>&1 || true 329 STASH_RESTORED=1 330 echo "✅ Restored local changes from stash." 331 else 332 echo "⚠️ Could not auto-apply $STASH_REF cleanly." 333 echo " Your changes are still preserved in stash." 334 echo " Review manually with: git stash show -p $STASH_REF" 335 fi 336} 337 338checkout_branch() { 339 local remote="$1" 340 local target_branch="$2" 341 342 if ! git show-ref --verify --quiet "refs/remotes/${remote}/${target_branch}"; then 343 echo "❌ Remote branch not found: ${remote}/${target_branch}" 344 exit 1 345 fi 346 347 local current_branch 348 current_branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)" 349 350 if [[ "$current_branch" == "$target_branch" ]]; then 351 return 0 352 fi 353 354 if git show-ref --verify --quiet "refs/heads/${target_branch}"; then 355 git switch "$target_branch" >/dev/null 2>&1 || git checkout "$target_branch" >/dev/null 2>&1 356 else 357 git switch -c "$target_branch" --track "${remote}/${target_branch}" >/dev/null 2>&1 || \ 358 git checkout -b "$target_branch" --track "${remote}/${target_branch}" >/dev/null 2>&1 359 fi 360} 361 362pull_latest() { 363 local remote="$1" 364 local branch="$2" 365 366 echo "⬇️ Fetching latest changes from $remote..." 367 git fetch "$remote" --prune 368 369 checkout_branch "$remote" "$branch" 370 git branch --set-upstream-to="${remote}/${branch}" "$branch" >/dev/null 2>&1 || true 371 372 echo "🔄 Pulling latest changes from ${remote}/${branch}..." 373 if ! git pull --ff-only "$remote" "$branch"; then 374 echo "ℹ️ Fast-forward pull failed, retrying with rebase..." 375 git pull --rebase "$remote" "$branch" 376 fi 377} 378 379reexec_with_latest_updater_if_changed() { 380 if [[ "$UPDATE_SH_REEXECED" == "1" ]]; then 381 return 0 382 fi 383 384 if [[ -z "$ORIGINAL_SCRIPT_CHECKSUM" ]]; then 385 return 0 386 fi 387 388 local current_checksum 389 current_checksum="$(compute_file_checksum "$SCRIPT_DIR/update.sh" 2>/dev/null || true)" 390 if [[ -z "$current_checksum" || "$current_checksum" == "$ORIGINAL_SCRIPT_CHECKSUM" ]]; then 391 return 0 392 fi 393 394 echo "♻️ update.sh was updated during pull. Restarting updater with newest logic..." 395 396 restore_stash_if_needed 397 trap - EXIT 398 cleanup 399 400 UPDATE_SH_REEXECED=1 exec bash "$SCRIPT_DIR/update.sh" "${ORIGINAL_ARGS[@]}" 401} 402 403native_module_compatible() { 404 run_bun -e "try{require('better-sqlite3');process.exit(0)}catch(e){console.error(e && e.message ? e.message : e);process.exit(1)}" >/dev/null 2>&1 405} 406 407rebuild_native_modules() { 408 if [[ "$DO_NATIVE_REBUILD" -eq 0 ]]; then 409 return 0 410 fi 411 412 if native_module_compatible; then 413 return 0 414 fi 415 416 echo "🔧 Native module mismatch detected, rebuilding..." 417 if run_bun run rebuild:native; then 418 return 0 419 fi 420 421 echo "⚠️ rebuild:native failed, forcing fresh Bun install..." 422 run_bun install --force 423} 424 425install_dependencies() { 426 if [[ "$DO_INSTALL" -ne 1 ]]; then 427 return 0 428 fi 429 430 echo "📦 Installing dependencies..." 431 run_bun install 432} 433 434build_project() { 435 if [[ "$DO_BUILD" -ne 1 ]]; then 436 return 0 437 fi 438 439 echo "🏗️ Building server + web dashboard..." 440 run_bun run build 441} 442 443pm2_has_process() { 444 local name="$1" 445 command -v pm2 >/dev/null 2>&1 && pm2 describe "$name" >/dev/null 2>&1 446} 447 448start_pm2_process_with_bun() { 449 local name="$1" 450 pm2 delete "$name" >/dev/null 2>&1 || true 451 pm2 start "$BUN_BIN" --name "$name" --cwd "$SCRIPT_DIR" --update-env -- dist/index.js 452} 453 454nohup_process_running() { 455 if [[ ! -f "$PID_FILE" ]]; then 456 return 1 457 fi 458 459 local pid 460 pid="$(cat "$PID_FILE" 2>/dev/null || true)" 461 if [[ -z "$pid" ]]; then 462 return 1 463 fi 464 465 if ! kill -0 "$pid" >/dev/null 2>&1; then 466 return 1 467 fi 468 469 local cmd 470 cmd="$(ps -p "$pid" -o command= 2>/dev/null || true)" 471 [[ "$cmd" == *"dist/index.js"* || "$cmd" == *"bun run start"* || "$cmd" == *"bun dist/index.js"* || "$cmd" == *"$APP_NAME"* ]] 472} 473 474restart_runtime() { 475 if [[ "$DO_RESTART" -ne 1 ]]; then 476 echo "⏭️ Skipping restart (--no-restart)." 477 return 0 478 fi 479 480 echo "🔄 Restarting runtime..." 481 482 if command -v pm2 >/dev/null 2>&1; then 483 local has_app=0 484 local has_legacy=0 485 486 if pm2_has_process "$APP_NAME"; then 487 has_app=1 488 fi 489 if pm2_has_process "$LEGACY_APP_NAME"; then 490 has_legacy=1 491 fi 492 493 if [[ "$has_app" -eq 1 && "$has_legacy" -eq 1 ]]; then 494 echo "ℹ️ Found both PM2 processes ($APP_NAME and $LEGACY_APP_NAME). Consolidating to $APP_NAME..." 495 echo "[pm2] Recreating $APP_NAME with Bun binary launcher" 496 start_pm2_process_with_bun "$APP_NAME" 497 echo "[pm2] Removing duplicate legacy process $LEGACY_APP_NAME" 498 pm2 delete "$LEGACY_APP_NAME" || true 499 echo "[pm2] Saving PM2 process list" 500 pm2 save || true 501 echo "✅ Restarted PM2 process: $APP_NAME" 502 return 0 503 fi 504 505 if [[ "$has_app" -eq 1 ]]; then 506 echo "[pm2] Recreating $APP_NAME with Bun binary launcher" 507 start_pm2_process_with_bun "$APP_NAME" 508 echo "[pm2] Saving PM2 process list" 509 pm2 save || true 510 echo "✅ Restarted PM2 process: $APP_NAME" 511 return 0 512 fi 513 514 if [[ "$has_legacy" -eq 1 ]]; then 515 echo "[pm2] Recreating legacy process $LEGACY_APP_NAME with Bun binary launcher" 516 start_pm2_process_with_bun "$LEGACY_APP_NAME" 517 echo "[pm2] Saving PM2 process list" 518 pm2 save || true 519 echo "✅ Restarted PM2 process: $LEGACY_APP_NAME" 520 return 0 521 fi 522 fi 523 524 if nohup_process_running; then 525 bash "$SCRIPT_DIR/install.sh" --start-only --nohup --skip-native-rebuild >/dev/null 526 echo "✅ Restarted nohup runtime." 527 return 0 528 fi 529 530 if command -v pm2 >/dev/null 2>&1; then 531 bash "$SCRIPT_DIR/install.sh" --start-only --pm2 --skip-native-rebuild >/dev/null 532 echo "✅ Started PM2 runtime (was not running)." 533 return 0 534 fi 535 536 bash "$SCRIPT_DIR/install.sh" --start-only --nohup --skip-native-rebuild >/dev/null 537 echo "✅ Started nohup runtime (was not running)." 538} 539 540print_summary() { 541 echo "" 542 echo "✅ Update complete!" 543 echo "" 544 echo "Current commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" 545 echo "Current branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')" 546 547 if [[ "$STASH_CREATED" -eq 1 ]]; then 548 if [[ "$STASH_RESTORED" -eq 1 ]]; then 549 echo "Stash restore: restored" 550 else 551 echo "Stash restore: pending manual apply ($STASH_REF)" 552 fi 553 fi 554} 555 556while [[ $# -gt 0 ]]; do 557 case "$1" in 558 --remote) 559 if [[ $# -lt 2 ]]; then 560 echo "Missing value for --remote" 561 exit 1 562 fi 563 REMOTE_OVERRIDE="$2" 564 shift 565 ;; 566 --branch) 567 if [[ $# -lt 2 ]]; then 568 echo "Missing value for --branch" 569 exit 1 570 fi 571 BRANCH_OVERRIDE="$2" 572 shift 573 ;; 574 --skip-install) 575 DO_INSTALL=0 576 ;; 577 --skip-build) 578 DO_BUILD=0 579 ;; 580 --skip-native-rebuild) 581 DO_NATIVE_REBUILD=0 582 ;; 583 --no-restart) 584 DO_RESTART=0 585 ;; 586 -h|--help) 587 usage 588 exit 0 589 ;; 590 *) 591 echo "Unknown option: $1" 592 usage 593 exit 1 594 ;; 595 esac 596 shift 597done 598 599echo "🔄 Tweets-2-Bsky Updater" 600echo "=========================" 601 602require_command git 603ensure_bun_runtime 604ensure_git_repo 605 606ORIGINAL_SCRIPT_CHECKSUM="$(compute_file_checksum "$SCRIPT_DIR/update.sh" 2>/dev/null || true)" 607 608acquire_lock 609trap cleanup EXIT 610 611backup_file "$CONFIG_FILE" 612backup_file "$ENV_FILE" 613 614stash_local_changes 615print_untracked_notice 616 617remote="$(resolve_remote)" 618branch="$(resolve_branch "$remote")" 619 620pull_latest "$remote" "$branch" 621reexec_with_latest_updater_if_changed 622install_dependencies 623rebuild_native_modules 624build_project 625restart_runtime 626restore_stash_if_needed 627print_summary