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 562 lines 14 kB view raw
1#!/usr/bin/env bash 2 3set -euo pipefail 4 5APP_NAME="tweets-2-bsky" 6SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 7cd "$SCRIPT_DIR" 8 9ENV_FILE="$SCRIPT_DIR/.env" 10APP_PORT="" 11TS_HTTPS_PORT="" 12TS_AUTHKEY="" 13TS_HOSTNAME="" 14USE_FUNNEL=0 15INSTALL_ARGS=() 16BUN_BIN="" 17 18usage() { 19 cat <<'USAGE' 20Usage: ./install-server.sh [options] 21 22Secure Linux VPS install for Tailscale-first access: 23 - Runs regular app installer 24 - Forces app bind to localhost only (HOST=127.0.0.1) 25 - Installs and starts Tailscale if needed 26 - Publishes app through Tailscale Serve (HTTPS on tailnet) 27 28Options: 29 --port <number> App port (default: 3000; auto-adjusts if already in use) 30 --https-port <number> Tailscale HTTPS serve port (auto-select if omitted) 31 --auth-key <key> Tailscale auth key for non-interactive login 32 --hostname <name> Optional Tailscale device hostname 33 --funnel Also enable Tailscale Funnel (public internet) 34 35Install passthrough options (forwarded to ./install.sh): 36 --no-start 37 --start-only 38 --pm2 39 --nohup 40 --skip-install 41 --skip-build 42 43 -h, --help Show this help 44USAGE 45} 46 47require_command() { 48 local command_name="$1" 49 if ! command -v "$command_name" >/dev/null 2>&1; then 50 echo "Required command not found: $command_name" 51 exit 1 52 fi 53} 54 55ensure_bun_runtime() { 56 install_latest_bun() { 57 if command -v curl >/dev/null 2>&1; then 58 curl -fsSL https://bun.sh/install | bash >/dev/null 59 return 0 60 fi 61 if command -v wget >/dev/null 2>&1; then 62 wget -qO- https://bun.sh/install | bash >/dev/null 63 return 0 64 fi 65 66 echo "❌ Bun is required, and curl/wget is unavailable for auto-install." 67 echo " Install Bun manually: https://bun.com/docs/installation" 68 exit 1 69 } 70 71 resolve_bun_bin() { 72 if command -v bun >/dev/null 2>&1; then 73 command -v bun 74 return 0 75 fi 76 if [[ -x "${HOME}/.bun/bin/bun" ]]; then 77 printf '%s\n' "${HOME}/.bun/bin/bun" 78 return 0 79 fi 80 return 1 81 } 82 83 if ! BUN_BIN="$(resolve_bun_bin)"; then 84 echo "📦 Bun not found. Installing latest Bun..." 85 install_latest_bun 86 BUN_BIN="$(resolve_bun_bin || true)" 87 fi 88 89 if [[ -z "$BUN_BIN" || ! -x "$BUN_BIN" ]]; then 90 echo "❌ Bun could not be resolved." 91 echo " Install Bun manually: https://bun.com/docs/installation" 92 exit 1 93 fi 94 95 export PATH="$(dirname "$BUN_BIN"):$PATH" 96 97 if ! "$BUN_BIN" upgrade >/dev/null 2>&1; then 98 echo "⚠️ Bun auto-upgrade failed. Reinstalling latest Bun..." 99 install_latest_bun 100 BUN_BIN="$(resolve_bun_bin || true)" 101 fi 102 103 if [[ -z "$BUN_BIN" || ! -x "$BUN_BIN" ]]; then 104 echo "❌ Bun could not be resolved after auto-upgrade." 105 echo " Install Bun manually: https://bun.com/docs/installation" 106 exit 1 107 fi 108 109 export PATH="$(dirname "$BUN_BIN"):$PATH" 110 111 local bun_major 112 bun_major="$($BUN_BIN --version | awk -F. '{print $1}' 2>/dev/null || echo 0)" 113 if [[ "$bun_major" -lt 1 ]]; then 114 echo "❌ Bun 1.x+ is required. Current: $($BUN_BIN --version 2>/dev/null || echo 'unknown')" 115 exit 1 116 fi 117} 118 119run_bun() { 120 "$BUN_BIN" "$@" 121} 122 123is_valid_port() { 124 local candidate="$1" 125 [[ "$candidate" =~ ^[0-9]+$ ]] || return 1 126 (( candidate >= 1 && candidate <= 65535 )) 127} 128 129is_local_port_free() { 130 local port="$1" 131 run_bun -e ' 132const net = require("node:net"); 133const port = Number(process.argv[1]); 134const server = net.createServer(); 135server.once("error", () => process.exit(1)); 136server.once("listening", () => server.close(() => process.exit(0))); 137server.listen(port, "127.0.0.1"); 138' "$port" >/dev/null 2>&1 139} 140 141find_next_free_local_port() { 142 local start_port="$1" 143 local port 144 for port in $(seq "$start_port" 65535); do 145 if is_local_port_free "$port"; then 146 printf '%s\n' "$port" 147 return 0 148 fi 149 done 150 return 1 151} 152 153ensure_linux() { 154 if [[ "$(uname -s)" != "Linux" ]]; then 155 echo "install-server.sh currently supports Linux only." 156 echo "Use ./install.sh on macOS or other environments." 157 exit 1 158 fi 159} 160 161ensure_sudo() { 162 if [[ "$(id -u)" -eq 0 ]]; then 163 return 164 fi 165 166 if ! command -v sudo >/dev/null 2>&1; then 167 echo "sudo is required to install and configure Tailscale on this host." 168 exit 1 169 fi 170} 171 172run_as_root() { 173 if [[ "$(id -u)" -eq 0 ]]; then 174 "$@" 175 return 176 fi 177 sudo "$@" 178} 179 180get_env_value() { 181 local key="$1" 182 if [[ ! -f "$ENV_FILE" ]]; then 183 return 0 184 fi 185 local line 186 line="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true)" 187 if [[ -z "$line" ]]; then 188 return 0 189 fi 190 printf '%s\n' "${line#*=}" 191} 192 193upsert_env_value() { 194 local key="$1" 195 local value="$2" 196 touch "$ENV_FILE" 197 local tmp_file 198 tmp_file="$(mktemp)" 199 awk -v key="$key" -v value="$value" ' 200 BEGIN { updated = 0 } 201 $0 ~ ("^" key "=") { 202 print key "=" value 203 updated = 1 204 next 205 } 206 { print } 207 END { 208 if (!updated) { 209 print key "=" value 210 } 211 } 212 ' "$ENV_FILE" > "$tmp_file" 213 mv "$tmp_file" "$ENV_FILE" 214} 215 216ensure_local_only_env() { 217 local configured_port 218 configured_port="$(get_env_value PORT)" 219 if [[ -n "$configured_port" && -z "${APP_PORT:-}" ]]; then 220 APP_PORT="$configured_port" 221 fi 222 if [[ -z "${APP_PORT:-}" ]]; then 223 APP_PORT="3000" 224 fi 225 226 if ! is_valid_port "$APP_PORT"; then 227 echo "Invalid port: $APP_PORT" 228 exit 1 229 fi 230 231 if ! is_local_port_free "$APP_PORT"; then 232 local requested_port="$APP_PORT" 233 APP_PORT="$(find_next_free_local_port "$APP_PORT" || true)" 234 if [[ -z "$APP_PORT" ]]; then 235 echo "Could not find a free local port for the app." 236 exit 1 237 fi 238 echo "⚠️ App port ${requested_port} is already in use. Using ${APP_PORT} instead." 239 fi 240 241 upsert_env_value PORT "$APP_PORT" 242 upsert_env_value HOST "127.0.0.1" 243} 244 245run_app_install() { 246 local install_cmd=(bash "$SCRIPT_DIR/install.sh" --port "$APP_PORT") 247 install_cmd+=("${INSTALL_ARGS[@]}") 248 "${install_cmd[@]}" 249} 250 251install_tailscale_if_needed() { 252 if command -v tailscale >/dev/null 2>&1; then 253 echo "✅ Tailscale already installed." 254 return 255 fi 256 257 echo "📦 Installing Tailscale..." 258 if command -v curl >/dev/null 2>&1; then 259 run_as_root bash -c 'curl -fsSL https://tailscale.com/install.sh | sh' 260 elif command -v wget >/dev/null 2>&1; then 261 run_as_root bash -c 'wget -qO- https://tailscale.com/install.sh | sh' 262 else 263 echo "Need curl or wget to install Tailscale automatically." 264 exit 1 265 fi 266 267 if ! command -v tailscale >/dev/null 2>&1; then 268 echo "Tailscale installation did not complete successfully." 269 exit 1 270 fi 271} 272 273ensure_tailscaled_running() { 274 echo "🔧 Ensuring tailscaled is running..." 275 if command -v systemctl >/dev/null 2>&1; then 276 run_as_root systemctl enable --now tailscaled 277 return 278 fi 279 280 if command -v rc-service >/dev/null 2>&1; then 281 run_as_root rc-service tailscaled start || true 282 if command -v rc-update >/dev/null 2>&1; then 283 run_as_root rc-update add tailscaled default || true 284 fi 285 return 286 fi 287 288 if command -v service >/dev/null 2>&1; then 289 run_as_root service tailscaled start || true 290 return 291 fi 292 293 echo "Could not detect init system to start tailscaled automatically." 294 echo "Please start tailscaled manually, then re-run this script." 295 exit 1 296} 297 298ensure_tailscale_connected() { 299 if run_as_root tailscale ip -4 >/dev/null 2>&1; then 300 echo "✅ Tailscale is already connected." 301 return 302 fi 303 304 echo "🔐 Connecting this host to Tailscale..." 305 local up_cmd=(tailscale up) 306 307 if [[ -n "$TS_AUTHKEY" ]]; then 308 up_cmd+=(--authkey "$TS_AUTHKEY") 309 fi 310 311 if [[ -n "$TS_HOSTNAME" ]]; then 312 up_cmd+=(--hostname "$TS_HOSTNAME") 313 fi 314 315 if ! run_as_root "${up_cmd[@]}"; then 316 echo "Failed to connect to Tailscale." 317 if [[ -z "$TS_AUTHKEY" ]]; then 318 echo "Tip: provide --auth-key for non-interactive server setup." 319 fi 320 exit 1 321 fi 322} 323 324get_used_tailscale_https_ports() { 325 local json 326 json="$(run_as_root tailscale serve status --json 2>/dev/null || true)" 327 if [[ -z "$json" || "$json" == "{}" ]]; then 328 return 0 329 fi 330 331 printf '%s' "$json" | run_bun -e ' 332const fs = require("node:fs"); 333try { 334 const data = JSON.parse(fs.readFileSync(0, "utf8")); 335 const sets = [data?.Web, data?.web, data?.TCP, data?.tcp]; 336 const used = new Set(); 337 for (const obj of sets) { 338 if (!obj || typeof obj !== "object") continue; 339 for (const key of Object.keys(obj)) { 340 if (/^\d+$/.test(key)) used.add(Number(key)); 341 } 342 } 343 process.stdout.write([...used].sort((a, b) => a - b).join("\n")); 344} catch {} 345' 346} 347 348pick_tailscale_https_port() { 349 local preferred="$1" 350 local allow_used_preferred="${2:-0}" 351 local used_ports 352 used_ports="$(get_used_tailscale_https_ports || true)" 353 354 local is_used=0 355 if [[ -n "$preferred" ]]; then 356 if ! is_valid_port "$preferred"; then 357 echo "Invalid Tailscale HTTPS port: $preferred" 358 exit 1 359 fi 360 if [[ -z "$used_ports" ]] || ! grep -qx "$preferred" <<<"$used_ports"; then 361 printf '%s\n' "$preferred" 362 return 0 363 fi 364 if [[ "$allow_used_preferred" -eq 1 ]]; then 365 printf '%s\n' "$preferred" 366 return 0 367 fi 368 is_used=1 369 fi 370 371 local candidate 372 for candidate in 443 8443 9443; do 373 if [[ -z "$used_ports" ]] || ! grep -qx "$candidate" <<<"$used_ports"; then 374 printf '%s\n' "$candidate" 375 return 0 376 fi 377 done 378 379 for candidate in $(seq 10000 65535); do 380 if [[ -z "$used_ports" ]] || ! grep -qx "$candidate" <<<"$used_ports"; then 381 printf '%s\n' "$candidate" 382 return 0 383 fi 384 done 385 386 if [[ "$is_used" -eq 1 ]]; then 387 echo "No free Tailscale HTTPS serve port available (preferred port is already used)." 388 else 389 echo "No free Tailscale HTTPS serve port available." 390 fi 391 exit 1 392} 393 394configure_tailscale_serve() { 395 local preferred_https_port="$TS_HTTPS_PORT" 396 local preferred_from_saved=0 397 local saved_https_port 398 saved_https_port="$(get_env_value TAILSCALE_HTTPS_PORT)" 399 if [[ -z "$preferred_https_port" && -n "$saved_https_port" ]]; then 400 preferred_https_port="$saved_https_port" 401 preferred_from_saved=1 402 fi 403 404 TS_HTTPS_PORT="$(pick_tailscale_https_port "$preferred_https_port" "$preferred_from_saved")" 405 upsert_env_value TAILSCALE_HTTPS_PORT "$TS_HTTPS_PORT" 406 407 if [[ -n "$preferred_https_port" && "$preferred_https_port" != "$TS_HTTPS_PORT" ]]; then 408 echo "⚠️ Tailscale HTTPS port ${preferred_https_port} is already used. Using ${TS_HTTPS_PORT}." 409 fi 410 411 echo "🌐 Configuring Tailscale Serve (HTTPS ${TS_HTTPS_PORT} -> localhost:${APP_PORT})..." 412 if ! run_as_root tailscale serve --https "$TS_HTTPS_PORT" --bg --yes "http://127.0.0.1:${APP_PORT}"; then 413 run_as_root tailscale serve --https "$TS_HTTPS_PORT" --bg "http://127.0.0.1:${APP_PORT}" 414 fi 415 416 if [[ "$USE_FUNNEL" -eq 1 ]]; then 417 echo "⚠️ Enabling Tailscale Funnel (public internet exposure)..." 418 if ! run_as_root tailscale funnel --https "$TS_HTTPS_PORT" --bg --yes "http://127.0.0.1:${APP_PORT}"; then 419 run_as_root tailscale funnel --https "$TS_HTTPS_PORT" --bg "http://127.0.0.1:${APP_PORT}" 420 fi 421 fi 422} 423 424get_tailscale_dns_name() { 425 local json 426 json="$(run_as_root tailscale status --json 2>/dev/null || true)" 427 if [[ -z "$json" ]]; then 428 return 0 429 fi 430 431 printf '%s' "$json" | run_bun -e ' 432const fs = require("node:fs"); 433try { 434 const data = JSON.parse(fs.readFileSync(0, "utf8")); 435 const dnsName = typeof data?.Self?.DNSName === "string" ? data.Self.DNSName : ""; 436 process.stdout.write(dnsName.replace(/\.$/, "")); 437} catch {} 438' 439} 440 441get_tailscale_ipv4() { 442 run_as_root tailscale ip -4 2>/dev/null | head -n 1 || true 443} 444 445print_summary() { 446 local dns_name 447 dns_name="$(get_tailscale_dns_name)" 448 local ts_ip 449 ts_ip="$(get_tailscale_ipv4)" 450 local final_url="" 451 local port_suffix="" 452 if [[ "$TS_HTTPS_PORT" != "443" ]]; then 453 port_suffix=":${TS_HTTPS_PORT}" 454 fi 455 if [[ -n "$dns_name" ]]; then 456 final_url="https://${dns_name}${port_suffix}" 457 elif [[ -n "$ts_ip" ]]; then 458 final_url="https://${ts_ip}${port_suffix}" 459 fi 460 461 echo "" 462 echo "Setup complete for Linux server mode." 463 echo "" 464 echo "App binding:" 465 echo " HOST=127.0.0.1 (local-only)" 466 echo " PORT=${APP_PORT}" 467 echo "" 468 echo "Local checks on server:" 469 echo " http://127.0.0.1:${APP_PORT}" 470 echo "" 471 echo "Tailnet access:" 472 if [[ -n "$final_url" ]]; then 473 echo " ${final_url}" 474 echo "" 475 echo "✅ It will be accessible on ${final_url} wherever that person is authenticated on Tailscale." 476 else 477 echo " Run: sudo tailscale status" 478 fi 479 480 if [[ "$USE_FUNNEL" -eq 1 ]]; then 481 echo "" 482 echo "Public access is enabled via Funnel." 483 else 484 echo "" 485 echo "Public internet exposure is disabled." 486 fi 487 488 echo "" 489 echo "Useful commands:" 490 echo " ./install.sh --status" 491 echo " sudo tailscale serve status" 492 if [[ "$USE_FUNNEL" -eq 1 ]]; then 493 echo " sudo tailscale funnel status" 494 fi 495} 496 497while [[ $# -gt 0 ]]; do 498 case "$1" in 499 --port) 500 if [[ $# -lt 2 ]]; then 501 echo "Missing value for --port" 502 exit 1 503 fi 504 APP_PORT="$2" 505 shift 506 ;; 507 --https-port) 508 if [[ $# -lt 2 ]]; then 509 echo "Missing value for --https-port" 510 exit 1 511 fi 512 TS_HTTPS_PORT="$2" 513 shift 514 ;; 515 --auth-key) 516 if [[ $# -lt 2 ]]; then 517 echo "Missing value for --auth-key" 518 exit 1 519 fi 520 TS_AUTHKEY="$2" 521 shift 522 ;; 523 --hostname) 524 if [[ $# -lt 2 ]]; then 525 echo "Missing value for --hostname" 526 exit 1 527 fi 528 TS_HOSTNAME="$2" 529 shift 530 ;; 531 --funnel) 532 USE_FUNNEL=1 533 ;; 534 --pm2|--nohup|--skip-install|--skip-build|--no-start|--start-only) 535 INSTALL_ARGS+=("$1") 536 ;; 537 -h|--help) 538 usage 539 exit 0 540 ;; 541 *) 542 echo "Unknown option: $1" 543 usage 544 exit 1 545 ;; 546 esac 547 shift 548done 549 550ensure_linux 551ensure_sudo 552require_command bash 553require_command git 554ensure_bun_runtime 555 556ensure_local_only_env 557run_app_install 558install_tailscale_if_needed 559ensure_tailscaled_running 560ensure_tailscale_connected 561configure_tailscale_serve 562print_summary