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