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 703f9d24ce296a631987698156d98372b707b2f6 414 lines 8.5 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" 10RUNTIME_DIR="$SCRIPT_DIR/data/runtime" 11PID_FILE="$RUNTIME_DIR/${APP_NAME}.pid" 12LOG_FILE="$RUNTIME_DIR/${APP_NAME}.log" 13 14ACTION="install" 15DO_INSTALL=1 16DO_BUILD=1 17DO_START=1 18RUNNER="auto" 19PORT_OVERRIDE="" 20APP_PORT="" 21ACTIVE_RUNNER="" 22CREATED_JWT_SECRET=0 23 24usage() { 25 cat <<'USAGE' 26Usage: ./install.sh [options] 27 28Default behavior: 29 - Installs dependencies 30 - Builds server + web app 31 - Starts in the background (PM2 if installed, otherwise nohup) 32 - Prints local web URL 33 34Options: 35 --no-start Install/build only (do not start background process) 36 --start-only Start background process only (skip install/build) 37 --stop Stop background process (PM2 and/or nohup) 38 --status Show background process status 39 --pm2 Force PM2 runner 40 --nohup Force nohup runner 41 --port <number> Set or override PORT in .env 42 --skip-install Skip npm install 43 --skip-build Skip npm run build 44 -h, --help Show this help 45USAGE 46} 47 48require_command() { 49 local command_name="$1" 50 if ! command -v "$command_name" >/dev/null 2>&1; then 51 echo "Required command not found: $command_name" 52 exit 1 53 fi 54} 55 56is_valid_port() { 57 local candidate="$1" 58 [[ "$candidate" =~ ^[0-9]+$ ]] || return 1 59 (( candidate >= 1 && candidate <= 65535 )) 60} 61 62get_env_value() { 63 local key="$1" 64 if [[ ! -f "$ENV_FILE" ]]; then 65 return 0 66 fi 67 local line 68 line="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 || true)" 69 if [[ -z "$line" ]]; then 70 return 0 71 fi 72 printf '%s\n' "${line#*=}" 73} 74 75upsert_env_value() { 76 local key="$1" 77 local value="$2" 78 touch "$ENV_FILE" 79 local tmp_file 80 tmp_file="$(mktemp)" 81 awk -v key="$key" -v value="$value" ' 82 BEGIN { updated = 0 } 83 $0 ~ ("^" key "=") { 84 print key "=" value 85 updated = 1 86 next 87 } 88 { print } 89 END { 90 if (!updated) { 91 print key "=" value 92 } 93 } 94 ' "$ENV_FILE" > "$tmp_file" 95 mv "$tmp_file" "$ENV_FILE" 96} 97 98ensure_env_defaults() { 99 local existing_port 100 existing_port="$(get_env_value PORT)" 101 if [[ -n "$PORT_OVERRIDE" ]]; then 102 APP_PORT="$PORT_OVERRIDE" 103 elif [[ -n "$existing_port" ]]; then 104 APP_PORT="$existing_port" 105 else 106 APP_PORT="3000" 107 fi 108 109 if ! is_valid_port "$APP_PORT"; then 110 echo "Invalid port: $APP_PORT" 111 exit 1 112 fi 113 114 if [[ -z "$existing_port" || -n "$PORT_OVERRIDE" ]]; then 115 upsert_env_value PORT "$APP_PORT" 116 fi 117 118 local existing_secret 119 existing_secret="$(get_env_value JWT_SECRET)" 120 if [[ -z "$existing_secret" ]]; then 121 local generated_secret 122 generated_secret="$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")" 123 upsert_env_value JWT_SECRET "$generated_secret" 124 CREATED_JWT_SECRET=1 125 fi 126} 127 128ensure_build_artifacts() { 129 if [[ ! -f "$SCRIPT_DIR/dist/index.js" ]]; then 130 echo "Build output not found (dist/index.js). Running build now." 131 npm run build 132 fi 133} 134 135install_and_build() { 136 if [[ "$DO_INSTALL" -eq 1 ]]; then 137 echo "Installing dependencies" 138 npm install 139 fi 140 141 if [[ "$DO_BUILD" -eq 1 ]]; then 142 echo "Building server and web app" 143 npm run build 144 fi 145} 146 147stop_nohup_if_running() { 148 if [[ ! -f "$PID_FILE" ]]; then 149 return 1 150 fi 151 152 local pid 153 pid="$(cat "$PID_FILE" 2>/dev/null || true)" 154 if [[ -z "$pid" ]]; then 155 rm -f "$PID_FILE" 156 return 1 157 fi 158 159 if kill -0 "$pid" >/dev/null 2>&1; then 160 kill "$pid" >/dev/null 2>&1 || true 161 rm -f "$PID_FILE" 162 return 0 163 fi 164 165 rm -f "$PID_FILE" 166 return 1 167} 168 169start_with_nohup() { 170 mkdir -p "$RUNTIME_DIR" 171 stop_nohup_if_running >/dev/null 2>&1 || true 172 173 echo "Starting with nohup" 174 nohup npm start > "$LOG_FILE" 2>&1 & 175 echo "$!" > "$PID_FILE" 176 177 local pid 178 pid="$(cat "$PID_FILE")" 179 sleep 1 180 if ! kill -0 "$pid" >/dev/null 2>&1; then 181 echo "Failed to start background process with nohup." 182 echo "Check logs: $LOG_FILE" 183 exit 1 184 fi 185} 186 187stop_pm2_if_running() { 188 if ! command -v pm2 >/dev/null 2>&1; then 189 return 1 190 fi 191 192 if pm2 describe "$APP_NAME" >/dev/null 2>&1; then 193 pm2 delete "$APP_NAME" >/dev/null 2>&1 || true 194 pm2 save >/dev/null 2>&1 || true 195 return 0 196 fi 197 198 return 1 199} 200 201start_with_pm2() { 202 echo "Starting with PM2" 203 204 if pm2 describe "twitter-mirror" >/dev/null 2>&1; then 205 pm2 delete "twitter-mirror" >/dev/null 2>&1 || true 206 fi 207 208 if pm2 describe "$APP_NAME" >/dev/null 2>&1; then 209 pm2 restart "$APP_NAME" --update-env >/dev/null 2>&1 210 else 211 pm2 start dist/index.js --name "$APP_NAME" --update-env >/dev/null 2>&1 212 fi 213 pm2 save >/dev/null 2>&1 || true 214} 215 216start_background() { 217 local resolved_runner="$RUNNER" 218 if [[ "$resolved_runner" == "auto" ]]; then 219 if command -v pm2 >/dev/null 2>&1; then 220 resolved_runner="pm2" 221 else 222 resolved_runner="nohup" 223 fi 224 fi 225 226 case "$resolved_runner" in 227 pm2) 228 require_command pm2 229 start_with_pm2 230 ACTIVE_RUNNER="pm2" 231 ;; 232 nohup) 233 start_with_nohup 234 ACTIVE_RUNNER="nohup" 235 ;; 236 *) 237 echo "Unsupported runner: $resolved_runner" 238 exit 1 239 ;; 240 esac 241} 242 243wait_for_web() { 244 if ! command -v curl >/dev/null 2>&1; then 245 return 0 246 fi 247 248 local url="http://127.0.0.1:${APP_PORT}" 249 local attempt 250 for ((attempt = 1; attempt <= 30; attempt++)); do 251 if curl -fsS "$url" >/dev/null 2>&1; then 252 return 0 253 fi 254 sleep 1 255 done 256 257 return 1 258} 259 260print_access_info() { 261 echo "" 262 echo "Setup complete." 263 echo "Web app URL: http://localhost:${APP_PORT}" 264 265 if [[ "$CREATED_JWT_SECRET" -eq 1 ]]; then 266 echo "Generated JWT_SECRET in .env" 267 fi 268 269 if [[ "$ACTIVE_RUNNER" == "pm2" ]]; then 270 echo "Process manager: PM2" 271 echo "Status: pm2 status $APP_NAME" 272 echo "Logs: pm2 logs $APP_NAME" 273 echo "Stop: pm2 delete $APP_NAME" 274 elif [[ "$ACTIVE_RUNNER" == "nohup" ]]; then 275 echo "Process manager: nohup" 276 echo "PID file: $PID_FILE" 277 echo "Logs: tail -f $LOG_FILE" 278 echo "Stop: ./install.sh --stop" 279 fi 280 281 if wait_for_web; then 282 echo "Health check: OK" 283 else 284 echo "Health check: not ready yet (service may still be starting)" 285 fi 286} 287 288show_status() { 289 local found=0 290 local configured_port 291 configured_port="$(get_env_value PORT)" 292 if [[ -n "$configured_port" ]]; then 293 echo "Configured PORT: $configured_port" 294 fi 295 296 if command -v pm2 >/dev/null 2>&1 && pm2 describe "$APP_NAME" >/dev/null 2>&1; then 297 found=1 298 echo "PM2 process is running:" 299 pm2 status "$APP_NAME" 300 fi 301 302 if [[ -f "$PID_FILE" ]]; then 303 local pid 304 pid="$(cat "$PID_FILE" 2>/dev/null || true)" 305 if [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1; then 306 found=1 307 echo "nohup process is running with PID $pid" 308 echo "Logs: $LOG_FILE" 309 else 310 echo "Found stale PID file at $PID_FILE" 311 fi 312 fi 313 314 if [[ "$found" -eq 0 ]]; then 315 echo "No running background process found for $APP_NAME" 316 fi 317} 318 319stop_all() { 320 local stopped=0 321 if stop_pm2_if_running; then 322 stopped=1 323 echo "Stopped PM2 process: $APP_NAME" 324 fi 325 326 if stop_nohup_if_running; then 327 stopped=1 328 echo "Stopped nohup process from PID file" 329 fi 330 331 if [[ "$stopped" -eq 0 ]]; then 332 echo "No running process found for $APP_NAME" 333 fi 334} 335 336while [[ $# -gt 0 ]]; do 337 case "$1" in 338 --no-start) 339 DO_START=0 340 ;; 341 --start-only) 342 ACTION="start" 343 DO_INSTALL=0 344 DO_BUILD=0 345 DO_START=1 346 ;; 347 --stop) 348 ACTION="stop" 349 ;; 350 --status) 351 ACTION="status" 352 ;; 353 --pm2) 354 RUNNER="pm2" 355 ;; 356 --nohup) 357 RUNNER="nohup" 358 ;; 359 --port) 360 if [[ $# -lt 2 ]]; then 361 echo "Missing value for --port" 362 exit 1 363 fi 364 PORT_OVERRIDE="$2" 365 shift 366 ;; 367 --skip-install) 368 DO_INSTALL=0 369 ;; 370 --skip-build) 371 DO_BUILD=0 372 ;; 373 -h|--help) 374 usage 375 exit 0 376 ;; 377 *) 378 echo "Unknown option: $1" 379 usage 380 exit 1 381 ;; 382 esac 383 shift 384done 385 386case "$ACTION" in 387 stop) 388 stop_all 389 exit 0 390 ;; 391 status) 392 show_status 393 exit 0 394 ;; 395esac 396 397require_command node 398require_command npm 399 400ensure_env_defaults 401 402if [[ "$ACTION" == "install" ]]; then 403 install_and_build 404fi 405 406if [[ "$DO_START" -eq 0 ]]; then 407 echo "Install/build complete. Start later with: ./install.sh --start-only" 408 echo "Configured web URL: http://localhost:${APP_PORT}" 409 exit 0 410fi 411 412ensure_build_artifacts 413start_background 414print_access_info