Skip to content

Instantly share code, notes, and snippets.

@splincode
Last active July 11, 2026 08:04
Show Gist options
  • Select an option

  • Save splincode/7f490c668bdbf321f4405da640c253b9 to your computer and use it in GitHub Desktop.

Select an option

Save splincode/7f490c668bdbf321f4405da640c253b9 to your computer and use it in GitHub Desktop.
Скрипт получает фолловеров со всех страниц, исключает уже существующие подписки и останавливается при первой ошибке.
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
TARGET="${1:-splincode}"
DELAY_MIN="${DELAY_MIN:-5}"
DELAY_MAX="${DELAY_MAX:-8}"
YES="${YES:-0}"
API_VERSION="${GITHUB_API_VERSION:-2026-03-10}"
command -v gh >/dev/null 2>&1 || {
echo "Ошибка: GitHub CLI не установлен."
exit 1
}
gh auth status -h github.com >/dev/null 2>&1 || {
echo "Ошибка: сначала авторизуйся через gh auth login."
exit 1
}
if ((DELAY_MIN > DELAY_MAX)); then
echo "Ошибка: DELAY_MIN не может быть больше DELAY_MAX."
exit 1
fi
gh_api() {
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: ${API_VERSION}" \
"$@"
}
random_delay() {
local range=$((DELAY_MAX - DELAY_MIN + 1))
echo $((RANDOM % range + DELAY_MIN))
}
WORK_DIR="$(mktemp -d)"
FOLLOWERS_FILE="${WORK_DIR}/followers"
TARGET_FOLLOWING_FILE="${WORK_DIR}/target-following"
TARGET_USERS_FILE="${WORK_DIR}/target-users"
MY_FOLLOWING_FILE="${WORK_DIR}/my-following"
TODO_FILE="${WORK_DIR}/todo"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
CURRENT_USER="$(gh_api user --jq '.login')"
echo "Активный аккаунт: @${CURRENT_USER}"
echo "Целевой аккаунт: @${TARGET}"
echo
echo "Получаю фолловеров @${TARGET}..."
gh_api \
--paginate \
"users/${TARGET}/followers?per_page=100" \
--jq '.[].login' \
>"$FOLLOWERS_FILE"
echo "Получаю пользователей, на которых подписан @${TARGET}..."
gh_api \
--paginate \
"users/${TARGET}/following?per_page=100" \
--jq '.[].login' \
>"$TARGET_FOLLOWING_FILE"
# Объединяем followers и following целевого пользователя,
# затем удаляем дубли.
cat \
"$FOLLOWERS_FILE" \
"$TARGET_FOLLOWING_FILE" |
sort -u \
>"$TARGET_USERS_FILE"
echo "Получаю текущие подписки @${CURRENT_USER}..."
gh_api \
--paginate \
"user/following?per_page=100" \
--jq '.[].login' |
sort -u \
>"$MY_FOLLOWING_FILE"
# Оставляем только пользователей, на которых мы еще не подписаны.
# Также исключаем собственный аккаунт.
comm -23 "$TARGET_USERS_FILE" "$MY_FOLLOWING_FILE" |
awk -v current_user="$CURRENT_USER" '$0 != current_user' \
>"$TODO_FILE"
FOLLOWERS_COUNT="$(
sort -u "$FOLLOWERS_FILE" |
wc -l |
tr -d '[:space:]'
)"
TARGET_FOLLOWING_COUNT="$(
sort -u "$TARGET_FOLLOWING_FILE" |
wc -l |
tr -d '[:space:]'
)"
UNIQUE_TARGET_COUNT="$(
wc -l <"$TARGET_USERS_FILE" |
tr -d '[:space:]'
)"
TOTAL="$(
wc -l <"$TODO_FILE" |
tr -d '[:space:]'
)"
echo
echo "Фолловеров @${TARGET}: ${FOLLOWERS_COUNT}"
echo "Подписок @${TARGET}: ${TARGET_FOLLOWING_COUNT}"
echo "Уникальных пользователей: ${UNIQUE_TARGET_COUNT}"
echo "Новых подписок для @${CURRENT_USER}: ${TOTAL}"
if [[ "$TOTAL" -eq 0 ]]; then
echo "Новых пользователей для подписки нет."
exit 0
fi
echo
echo "Пауза между запросами: случайно от ${DELAY_MIN} до ${DELAY_MAX} секунд."
if [[ "$YES" != "1" ]]; then
read -r -p "Продолжить? [y/N] " ANSWER
if [[ ! "$ANSWER" =~ ^[Yy]$ ]]; then
echo "Отменено."
exit 0
fi
fi
SUCCESS=0
FAILED=0
PROCESSED=0
while IFS= read -r USERNAME; do
[[ -z "$USERNAME" ]] && continue
PROCESSED=$((PROCESSED + 1))
printf '[%d/%d] Подписка на @%s... ' \
"$PROCESSED" \
"$TOTAL" \
"$USERNAME"
if gh_api \
--method PUT \
"user/following/${USERNAME}" \
--silent; then
echo "OK"
SUCCESS=$((SUCCESS + 1))
else
echo "ERROR"
FAILED=$((FAILED + 1))
echo "Скрипт остановлен, чтобы не продолжать после ошибки."
break
fi
# После последнего пользователя ждать уже не нужно.
if ((PROCESSED < TOTAL)); then
DELAY="$(random_delay)"
echo "Пауза: ${DELAY} сек."
sleep "$DELAY"
fi
done <"$TODO_FILE"
echo
echo "Готово."
echo "Успешных подписок: ${SUCCESS}"
echo "Ошибок: ${FAILED}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment