Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
8f11b1a
Fix NOT NULL constraint failed on repositories.repo_name for legacy DBs
devin-ai-integration[bot] Apr 28, 2026
690b6dd
Fix SEO httpx proxy kwarg compat + limit screenshots to 1
Apr 28, 2026
979b7dd
Deduplicate repo names per account on task retry
Apr 28, 2026
509db92
Align httpx version threshold + handle post-TOTP recovery verification
Apr 28, 2026
1d9a16c
Preserve FK/UNIQUE in legacy table rebuild; skip TOTP branch without …
Apr 28, 2026
e2629c9
Commit screenshot via API to assets/, AI-gen release notes, BAN_CHECK…
Apr 28, 2026
c5316ac
Screenshot at repo root, release AI no longer gated by README JSON er…
Apr 28, 2026
2887f4b
Preserve original AUTOINCREMENT (or lack of it) in _rebuild_table_wit…
Apr 29, 2026
a757ecc
Fix screenshot tokenizer, tolerant JSON, topic dedup, more ban-words
Apr 30, 2026
87e850a
Remove groq from AI cascade — endpoint chronic 404 on hardcoded model
mingojce Apr 30, 2026
31ef1d2
Cerebras model gpt-oss-120b; screenshots list empty folders + last-re…
mingojce May 1, 2026
cfcdf7a
GHP token reissue: browser worker, orchestrator dispatch, bot button
mingojce May 1, 2026
86f3881
Add .gitignore; untrack __pycache__ and logs/
mingojce May 1, 2026
6da5287
ai_worker: don't strip // inside JSON strings (preserve URLs)
mingojce May 1, 2026
19bb634
token_reissue: handle GitHub sudo-mode 2FA prompt before form
mingojce May 1, 2026
e2d25a2
token_reissue: fix 'await None' TypeError on every reissue
mingojce May 1, 2026
ae30616
token_reissue: ?type=classic + wider description selectors + diagnost…
mingojce May 1, 2026
cf73a9c
token_reissue: dedicated sudo-mode 2FA handler (no URL check)
mingojce May 1, 2026
751ebaa
token_reissue: iterate recovery codes + consume used one in DB
mingojce May 1, 2026
45d077e
token_reissue: align disappearance selectors with detection set
mingojce May 1, 2026
49de6f8
token_reissue: remove redundant _warmup_proxy call
mingojce May 1, 2026
6be997d
search: README variation + humanize commits + Discussions/Issues seeding
mingojce May 3, 2026
54debed
orch: don't cancel future-scheduled tasks on startup cleanup
mingojce May 3, 2026
091fe91
readme_variation: fix template #3 — {designed} used as adjective
mingojce May 3, 2026
7a4384d
humanize_commit: drop ('--' → '—') typo rule that corrupted MD/CLI
mingojce May 3, 2026
36b3104
ai_worker: stringify body in generate_release_metadata
mingojce May 3, 2026
05e5ec7
orch: recognise {ok: False} as failed task result
mingojce May 3, 2026
001c7c1
ai_worker: add OpenRouter to provider cascade
mingojce May 3, 2026
647a48b
browser_worker: keyword-rich SEO alt text for README screenshots
mingojce May 3, 2026
89adfa6
browser_worker: cross-link README to existing repos in DB
mingojce May 3, 2026
b46cc03
browser_worker: stage CI workflow + .gitignore + CI badge
mingojce May 3, 2026
c3e5a74
pinning_worker: pin top repos to user profile via UI
mingojce May 3, 2026
28a4424
wiki_seeder: push 5-page wiki to <repo>.wiki.git via git push
mingojce May 3, 2026
84a5202
orchestrator+bot: wire SEED_WIKI / PIN_TOP_REPOS / PIN_FOR_ACCOUNT
mingojce May 3, 2026
bbb0353
Address Devin Review: 3 new findings on PR #1
mingojce May 3, 2026
d347157
pinning_worker: use GitHub username (not email) for profile URL
mingojce May 3, 2026
7bcee51
token_reissue: don't treat HTTP 403/429 as invalid token
mingojce May 3, 2026
f41388b
Add account aging worker (stars/follows/reactions via API)
mingojce May 4, 2026
edf0fff
Add profile README worker (<user>/<user> repo with bio + projects)
mingojce May 4, 2026
9d290e4
Add multi-language README worker (ru/zh-CN/es/pt-BR translations)
mingojce May 4, 2026
60089e6
Add trending stars worker (daily cross-account engagement)
mingojce May 4, 2026
f4c6984
Skip I18N_README scheduling when AI cascade is not configured
mingojce May 4, 2026
ade39df
Surface failure details in queue logger and DB log
mingojce May 4, 2026
a4c0b9c
TRENDING_STARS: per-account fallback theme + page offset to break bat…
mingojce May 4, 2026
4c564a7
Fix 4 issues: create-repo timeout, name variation, ban persistence, 2…
mingojce May 5, 2026
ab21a26
Add singleton lock to prevent duplicate bot instances
mingojce May 9, 2026
007dbcd
Probe github.com/login (HTML) when validating proxies, not just API
mingojce May 9, 2026
43c4527
Make proxy web-probe lenient: don't drop API-alive proxies on web tim…
mingojce May 9, 2026
6bbaa79
Add 'disable proxies' kill-switch for diagnostics
mingojce May 9, 2026
24c470b
Stars button does stars only (was also forking + watching)
mingojce May 11, 2026
cd9dade
Daily TG summary card at 22:00 UTC
mingojce May 11, 2026
85a0787
Bot: replace release asset via TG upload (📦 Релизы button)
mingojce May 11, 2026
c5e1185
Bot: SETUP_PAGES retrofit task (extra indexable URL per repo)
mingojce May 11, 2026
045f13b
code_seeder: pump real source files (not just README) into every repo
mingojce May 11, 2026
d7e4fd7
prewarmup_worker: one-time typo-fix PR to popular OSS per account
mingojce May 11, 2026
77dec18
bot: keep release scope on invalid upload, let user retry
mingojce May 11, 2026
970522c
bot: key release upload dicts by user_id (not chat_id)
mingojce May 11, 2026
0363106
prewarmup: resolve real GitHub login via /user, fix loguru fmt strings
mingojce May 11, 2026
ad048de
prewarmup: drop archived EddieHub target, pre-flight check archived/d…
mingojce May 11, 2026
2d290a2
seo_github_worker: GET-before-PUT to make Pages deploy idempotent on …
mingojce May 11, 2026
b870c60
boost_worker: return structured dict instead of summed counter
mingojce May 11, 2026
f389abc
trending_stars: progressive query widening when 0 candidates
mingojce May 11, 2026
e1795ef
bot: raise release-zip cap to 100MB, warn about 20MB cloud Bot API limit
mingojce May 11, 2026
dea4f19
boost_worker: align early-exit returns with success-path dict shape
mingojce May 11, 2026
b607437
bot: widen seed/wiki recent batch filter to include created+boosted
mingojce May 11, 2026
18ea9da
code_seeder: fix __all__ ImportError on 'from pkg import *' for scaffold
mingojce May 11, 2026
ac57a2b
pinning_worker: widen candidate status filter to include created+boosted
mingojce May 11, 2026
9086e85
feat: Topics & About — назначить тематические teги без банвордов
mingojce May 11, 2026
7c9ec89
feat: support uploads up to 2GB via self-hosted telegram-bot-api
mingojce May 11, 2026
7ff6b72
feat(bot): accept release archives via URL (gofile / drive / yandex /…
mingojce May 12, 2026
b8a3e5e
fix(url-download): use rotating X-Website-Token for gofile + accept .…
mingojce May 12, 2026
988fa2c
fix(url-download): retry gofile API with exponential backoff on 5xx
mingojce May 12, 2026
4f845fa
feat(release): TG notifications for RELEASE_REPLACE batches
mingojce May 12, 2026
8fcee98
fix(main): persist last_run_date for daily loops across restarts
mingojce May 12, 2026
a130996
fix(2fa): match recovery codes case-insensitively when consuming from DB
mingojce May 12, 2026
39c41e1
fix(bot): restore release upload state when TG download fails
mingojce May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
__pycache__/
*.pyc
*.pyo
logs/
*.log
.venv/
venv/
.env
data/*.db
data/*.db-journal
cookies/
screenshots/validator/
.DS_Store
.bot.pid
344 changes: 344 additions & 0 deletions account_aging_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
"""Account aging — превращает «холодный» GitHub-аккаунт в активного юзера.

Свежесозданный аккаунт, который только постит свои репо и больше нигде
не появляется — самый простой паттерн для anti-spam GitHub: ноль
star'ов на чужие репо, ноль follow'ов, ноль реакций. Этот воркер
прогоняет аккаунт через имитацию реального поведения **через REST
API** (без браузера — быстро и без палевных camoufox-сессий):

1. Найти 10-15 популярных репо в его теме (search API,
``stars:>500`` фильтр).
2. ⭐ Звездануть 5-10 случайных из них (``PUT /user/starred/...``).
3. 👤 Зафолловить 3-5 владельцев этих репо
(``PUT /user/following/...``).
4. 👍 Поставить thumbsup-реакцию на 2-3 популярных открытых issue
в одном из этих репо (``POST /issues/<n>/reactions``). Текст
**не пишем** — это самый низко-рискованный вариант engagement'а.

После прогона аккаунт выглядит как dev который что-то скрейпил,
интересовался темой, наблюдает за проектами. Его собственные репо
ранжируются ощутимо выше — GitHub Search учитывает «соц.граф» в
своих сигналах.

Интерфейс:

age_account(token, themes, db_log, intensity='normal') -> dict

Возвращает ``{ok, starred, followed, reactions, error?}``.

Интенсивности:
* ``'light'`` — 3 stars / 1 follow / 1 reaction (для уже обкатанных
акков)
* ``'normal'`` — 7 stars / 4 follows / 2 reactions
* ``'heavy'`` — 12 stars / 6 follows / 3 reactions (новые акки)

Все ошибки тихие — возвращаем ``ok: False`` с причиной.
"""
from __future__ import annotations

import asyncio
import random
from typing import Optional

import httpx

from logger_setup import get_logger

log = get_logger(__name__)


REST_BASE = "https://api.github.com"


_INTENSITY_PROFILES = {
"light": {"stars": 3, "follows": 1, "reactions": 1, "delay_min": 5, "delay_max": 12},
"normal": {"stars": 7, "follows": 4, "reactions": 2, "delay_min": 8, "delay_max": 20},
"heavy": {"stars": 12, "follows": 6, "reactions": 3, "delay_min": 12, "delay_max": 30},
}


# Пул трендовых тем — поиск ведём по этим. Если тема нашего акка не
# попадает в whitelist, всё равно используем её прямо как `q=...`.
_THEME_TO_QUERY = {
"windows": "topic:windows topic:utility stars:>500",
"gaming": "topic:gaming stars:>500",
"overlay": "topic:overlay stars:>200",
"automation": "topic:automation stars:>500",
"cli": "topic:cli-tool stars:>500",
"python": "language:python topic:utility stars:>1000",
"javascript": "language:javascript topic:tool stars:>1000",
"rust": "language:rust topic:cli stars:>500",
"go": "language:go topic:devops stars:>500",
}


_REACTIONS = ("+1", "heart", "rocket", "hooray")


def _build_query(themes: list[str]) -> str:
"""Собрать GitHub-search query из тем аккаунта.

Если тема прямой match'ит наш whitelist — берём подготовленный
шаблон. Иначе — используем тему как ключевое слово плюс
``stars:>200`` чтобы отсечь мёртвые проекты.
"""
if not themes:
return "stars:>1000 topic:tool"
chunks: list[str] = []
for t in themes:
t = (t or "").strip().lower()
if not t:
continue
if t in _THEME_TO_QUERY:
chunks.append(_THEME_TO_QUERY[t])
else:
chunks.append(f"{t} stars:>200")
return " ".join(chunks[:3]) if chunks else "stars:>1000 topic:tool"


async def _search_popular_repos(
client: httpx.AsyncClient,
token: str,
themes: list[str],
limit: int = 25,
) -> list[dict]:
"""``GET /search/repositories?q=<theme>&sort=stars`` — топ репо.

Возвращает список словарей с ``full_name``, ``owner.login``,
``open_issues_count``. Логин для follow'ов берётся из ``owner``.
"""
q = _build_query(themes)
try:
r = await client.get(
f"{REST_BASE}/search/repositories",
params={
"q": q, "sort": "stars", "order": "desc",
"per_page": min(int(limit), 50),
},
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
)
if r.status_code != 200:
log.warning(
"[AGING] search_repos HTTP %s for q=%r: %s",
r.status_code, q, r.text[:200],
)
return []
items = r.json().get("items", [])
# Защита от пустого ответа.
return [it for it in items if it.get("full_name")]
except Exception as e:
log.warning("[AGING] search_repos crash: %s", e)
return []


async def _star_repo(
client: httpx.AsyncClient, token: str, full_name: str,
) -> bool:
"""``PUT /user/starred/{owner}/{repo}`` — 204 No Content при успехе."""
try:
r = await client.put(
f"{REST_BASE}/user/starred/{full_name}",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"Content-Length": "0",
},
)
return r.status_code in (204, 304) # 304 если уже звезда
except Exception as e:
log.warning("[AGING] star %s failed: %s", full_name, e)
return False


async def _follow_user(
client: httpx.AsyncClient, token: str, login: str,
) -> bool:
"""``PUT /user/following/{login}`` — 204 при успехе."""
try:
r = await client.put(
f"{REST_BASE}/user/following/{login}",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"Content-Length": "0",
},
)
return r.status_code in (204, 304)
except Exception as e:
log.warning("[AGING] follow %s failed: %s", login, e)
return False


async def _list_open_issues(
client: httpx.AsyncClient, token: str, full_name: str, limit: int = 5,
) -> list[int]:
"""Список номеров популярных open-issues для целевого репо."""
try:
r = await client.get(
f"{REST_BASE}/repos/{full_name}/issues",
params={
"state": "open", "sort": "comments", "direction": "desc",
"per_page": min(int(limit), 30),
},
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
},
)
if r.status_code != 200:
return []
items = r.json() or []
# Отсеиваем pull request (REST issues возвращает PR'ы тоже).
return [
it["number"] for it in items
if it.get("number") and not it.get("pull_request")
]
except Exception as e:
log.warning("[AGING] list_issues %s: %s", full_name, e)
return []


async def _react_to_issue(
client: httpx.AsyncClient, token: str, full_name: str,
issue_number: int, content: str = "+1",
) -> bool:
"""``POST /repos/{owner}/{repo}/issues/{n}/reactions`` — 200/201 при успехе."""
try:
r = await client.post(
f"{REST_BASE}/repos/{full_name}/issues/{issue_number}/reactions",
json={"content": content},
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
},
)
return r.status_code in (200, 201)
except Exception as e:
log.warning(
"[AGING] react %s#%d failed: %s", full_name, issue_number, e,
)
return False


async def age_account(
*,
token: str,
themes: Optional[list[str]] = None,
intensity: str = "normal",
db_log=None,
) -> dict:
"""Прогнать аккаунт через серию «реалистичных» действий.

:param token: PAT владельца аккаунта (нужен scope ``user:follow``,
``public_repo`` для star/reactions).
:param themes: список тем-ключевых слов (обычно из репо аккаунта
или из выбранной темы при создании).
:param intensity: light / normal / heavy (см. _INTENSITY_PROFILES).
:param db_log: optional callable ``(level, message)`` для записи
в БД. Без него только в stdout-лог.

Возвращает ``{ok, starred, followed, reactions, error?}``.
"""
if not token:
return {
"ok": False, "starred": 0, "followed": 0, "reactions": 0,
"error": "no_token",
}
profile = _INTENSITY_PROFILES.get(intensity, _INTENSITY_PROFILES["normal"])
themes = themes or []
delay_min = profile["delay_min"]
delay_max = profile["delay_max"]

starred = followed = reactions = 0

async with httpx.AsyncClient(
timeout=30, follow_redirects=True,
) as client:
repos = await _search_popular_repos(
client, token, themes,
limit=max(profile["stars"] * 2, 20),
)
if not repos:
return {
"ok": False, "starred": 0, "followed": 0, "reactions": 0,
"error": "no_search_results",
}

random.shuffle(repos)

# 1) Звёзды.
star_targets = repos[: profile["stars"]]
for repo in star_targets:
ok = await _star_repo(client, token, repo["full_name"])
if ok:
starred += 1
log.info("[AGING] ⭐ %s", repo["full_name"])
await asyncio.sleep(
random.uniform(delay_min, delay_max),
)

# 2) Follow'ы — берём владельцев части начвёзженных репо
# (рандомизация, чтобы не было паттерна «фолловлю всех кого
# звезданул»).
follow_pool = list({
(r.get("owner") or {}).get("login")
for r in star_targets
if (r.get("owner") or {}).get("login")
and (r.get("owner") or {}).get("type") != "Organization"
})
random.shuffle(follow_pool)
for login in follow_pool[: profile["follows"]]:
ok = await _follow_user(client, token, login)
if ok:
followed += 1
log.info("[AGING] 👤 follow %s", login)
await asyncio.sleep(
random.uniform(delay_min, delay_max),
)

# 3) Реакции на популярные open-issues. Берём первый из
# звёзданутого пула, чьи issues есть.
for repo in star_targets:
if reactions >= profile["reactions"]:
break
issue_numbers = await _list_open_issues(
client, token, repo["full_name"], limit=5,
)
if not issue_numbers:
continue
chosen_issue = random.choice(issue_numbers)
content = random.choice(_REACTIONS)
ok = await _react_to_issue(
client, token, repo["full_name"], chosen_issue, content,
)
if ok:
reactions += 1
log.info(
"[AGING] %s on %s#%d",
content, repo["full_name"], chosen_issue,
)
await asyncio.sleep(
random.uniform(delay_min, delay_max),
)

summary = (
f"aged: starred={starred}/{profile['stars']}, "
f"followed={followed}/{profile['follows']}, "
f"reactions={reactions}/{profile['reactions']}"
)
log.info("[AGING] %s", summary)
if db_log is not None:
try:
await db_log("INFO", summary)
except Exception:
pass
return {
"ok": (starred + followed + reactions) > 0,
"starred": starred,
"followed": followed,
"reactions": reactions,
}
Loading