From daaeba8be8df4d0876312e540bdd420e16ff724c Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sat, 11 Apr 2026 23:02:32 -0500 Subject: validate scraped usernames before enqueueing actors --- src/srht_contrib/scripts/enqueue_actors.py | 27 ++++++++++++++++++++------- tests/test_ingestion.py | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/srht_contrib/scripts/enqueue_actors.py b/src/srht_contrib/scripts/enqueue_actors.py index f6999af..03f1f9b 100644 --- a/src/srht_contrib/scripts/enqueue_actors.py +++ b/src/srht_contrib/scripts/enqueue_actors.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse from datetime import UTC, datetime, timedelta from pathlib import Path +import re from sqlalchemy import select @@ -11,19 +12,31 @@ from srht_contrib.db import make_session_factory from srht_contrib.models import TrackedActor +USERNAME_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,61}[A-Za-z0-9])?$") + + +def _normalize_actor(raw_username: str) -> str | None: + username = raw_username.strip() + if not username or username.startswith("#"): + return None + if username.startswith("~"): + username = username[1:] + if not USERNAME_RE.fullmatch(username): + return None + return f"~{username}" + + def _iter_usernames(path: Path) -> list[str]: usernames: list[str] = [] seen: set[str] = set() for raw_line in path.read_text(encoding="utf-8").splitlines(): - username = raw_line.strip() - if not username or username.startswith("#"): + actor = _normalize_actor(raw_line) + if actor is None: continue - if not username.startswith("~"): - username = f"~{username}" - if username in seen: + if actor in seen: continue - seen.add(username) - usernames.append(username) + seen.add(actor) + usernames.append(actor) return usernames diff --git a/tests/test_ingestion.py b/tests/test_ingestion.py index 974dac6..6a1a162 100644 --- a/tests/test_ingestion.py +++ b/tests/test_ingestion.py @@ -738,3 +738,27 @@ def test_enqueue_actors_staggers_without_polling(tmp_path, monkeypatch) -> None: assert actors[0].next_poll_after == queued_at.replace(tzinfo=None) assert actors[1].next_poll_after == (queued_at + timedelta(seconds=60)).replace(tzinfo=None) assert actors[2].next_poll_after == (queued_at + timedelta(seconds=120)).replace(tzinfo=None) + + +def test_enqueue_actors_skips_invalid_usernames(tmp_path, monkeypatch) -> None: + database_path = tmp_path / "enqueue-invalid.db" + username_path = tmp_path / "srht_usernames.txt" + username_path.write_text("-0\n.\n~bad-\nvalid_user\nok.ok\n", encoding="utf-8") + monkeypatch.setenv("DATABASE_URL", f"sqlite:///{database_path}") + monkeypatch.setenv("SRHT_TOKEN", "test-token") + monkeypatch.setenv("DEFAULT_ACTOR", "~ccleberg") + + from srht_contrib.db import Base, make_engine, make_session_factory + + settings = Settings() + engine = make_engine(settings) + Base.metadata.create_all(bind=engine) + session_factory = make_session_factory(settings) + + inserted = enqueue_actors(Path(username_path), stagger_seconds=60) + + with session_factory() as db: + actors = db.scalars(select(TrackedActor).order_by(TrackedActor.actor)).all() + + assert inserted == 2 + assert [actor.actor for actor in actors] == ["~ok.ok", "~valid_user"] -- cgit v1.2.3