Skip to content

WikiZEIT/bot

Repository files navigation

WikiZEIT Bot

CD/CD Offical website HTMLMinifier GitHub repo

A pywikibot-based bot for the Polish Wikipedia, running as WikiZEITBot.

The bot scans pages in the category Kategoria:Strony monitorowane przez bota WikiZEIT, looks for a known template invocation on each page, dispatches by template name to the matching handler class, and writes the rendered result back to the page (using paginated subpages when the output is large).

The bot-managed content on the main page is wrapped in HTML comment markers placed after the template invocation:

{{TemplateName|…params…}}
<!-- WikiZEITBot:TemplateName|…params… -->
…rendered content…
<!-- /WikiZEITBot:TemplateName -->
  • First run: the template stays put; the bot inserts the begin marker + content + end marker on the next line. Any text before the template and any text after the template are preserved.
  • Subsequent runs: the bot finds the template + matching markers and replaces only the content between the markers. Text before the begin marker (including the template and any notes you add between them) and text after the end marker are untouched.
  • Begin marker without a matching end marker (legacy / partial state): the bot replaces everything from the begin marker to the end of the page, restoring the proper end marker.
  • Template missing but markers exist (a previous version of the bot removed the template): the bot reconstructs the template invocation from the marker's params and re-injects it back into the page, then proceeds with the normal flow. Self-healing — no manual restore needed.

Params are always read from the template when it's present (the template is the authoritative source). Change params by editing the template, not the marker. Subpages stay fully bot-managed (no markers); they're created by the bot and aren't expected to host user content.

Supported wiki-side templates

  • {{Podopieczni|przewodnik=<mentor>|limit=<n>|edycje=<k>|email=tak}} — list mentees of the given mentor. przewodnik is the mentor username (required); limit overrides the pagination size (mentees per page, defaults to MenteesHandler.items_per_page = 200); edycje controls the limit= passed to the per-mentee {{Specjalna:Wkład/...}} transclusion (defaults to 5); email=tak opts the mentor into a personal e-mail summary of their own new mentees on each --summary run (see Email notifications). Blocked users and members of the editor/sysop groups are filtered out. When USE_SQL is on, only mentees who edited within LAST_EDIT_CUTOFF_DAYS survive, sorted by last-edit timestamp (descending); otherwise the remainder is sorted by editcount. The list is split into subpages of MenteesHandler.items_per_page mentees each, named <page>/2, /3, … (padded to the width of the largest index).

  • {{Fotografia}} (all params optional: fotograf, źródło, limit, nagłówek, próg dni, nazwa pliku, mime, etykieta, attrybuty) — gallery of the most recent Commons uploads. The user list comes from fotograf (one or more usernames) or źródło (a wiki page to scrape). fotograf takes precedence when both are set.

    • fotograf=<user> — single user. With nagłówek=<text> adds === <text> === as an H3 header; without, just the bare gallery. No active/inactive split.
    • fotograf=<user1>, <user2>, … — multi-user. Galleries render in the order typed, each with the standard plainlinks-styled H3 header. nagłówek and próg dni are ignored.
    • źródło=<page> (or omitted; default Wikiprojekt:Fotografia/Uczestnicy; namespace-agnostic — Wikipedia:Projekty_szkolne_i_akademickie/<projekt>, Wikipedysta:<user>/galeria, Pomoc:<foo>, etc. all work) — the page wikitext is scraped for every [[User:...]], [[Wikipedysta:...]], or [[:user:...]] link (any context — tables, lists, prose; /Subpage paths reduce to the bare username; duplicates deduped, first occurrence wins). Photographers split into == Aktywni == and == Nieaktywni == sections by próg dni (default 90 days); each user gets a plainlinks H3 header. Sections are sorted alphabetically (case-fold).

    If źródło is set but the page doesn't exist, the body becomes <!-- nie znaleziono strony źródłowej: <name> -->.

    limit applies in every mode and caps the per-user upload count (default 10, max 100).

    próg dni only applies in źródło mode and only when the active/inactive split is on:

    • próg dni=<n> (numeric, default 90) — split into == Aktywni == (last upload within n days) and == Nieaktywni == (older or no uploads).
    • próg dni=nie — disable the split; render one flat alphabetical list of photographers with no == Aktywni == / == Nieaktywni == headers.
    • In fotograf (single/multi) modes, próg dni is always ignored — galleries are shown regardless of how recently the user last uploaded.

    Users with no Commons uploads still get their H3 + <!-- brak zdjęć --> comment in page and multi modes; in single mode the comment replaces the gallery.

    Filtering and caption params (orthogonal to the mode):

    • nazwa pliku=<regex> — case-insensitive REGEXP substring search against img_name at SQL level. Example: nazwa pliku=\.jpe?g$ matches .jpg/.jpeg extensions; nazwa pliku=Wzlot matches any file with Wzlot anywhere in the name.
    • mime=<type> — exact MIME filter. mime=image/jpeg constrains both img_major_mime and img_minor_mime; mime=image constrains only the major type.
    • etykieta=<wiki code> — custom caption template for each gallery entry. Variables in the form {{token}} are replaced with values from the SQL row; unknown tokens (including literal templates like {{own}}) are left untouched. Default: <center>[[:commons:File:{{plik}}|{{plik}}]]</center>.
    • attrybuty=<text> — extra attributes added to the opening <gallery> tag. Empty by default (tag stays as <gallery>, no trailing space). Example: attrybuty=mode{{=}}packed<gallery mode=packed>.

    Param-value escapes (MediaWiki convention, recognized in every param):

    • {{=}} → literal = (needed because the first = separates key from value).
    • {{!}} → literal | (needed because | separates params at the top level).

    Available etykieta variables (Polish; English aliases also work — see translations.json):

    Token Source / format
    {{plik}} / {{filename}} img_name (spaces, no underscores)
    {{rozmiar}} / {{size}} img_size in bytes
    {{szerokość}} / {{width}} img_width in pixels
    {{wysokość}} / {{height}} img_height in pixels
    {{data}} / {{date}} upload date as YYYY-MM-DD
    {{czas}} / {{time}} upload time as ISO YYYY-MM-DDTHH:MM:SSZ
    {{ts}} raw 14-char MediaWiki timestamp
    {{sha1}} base36 hash
    {{mime}} major/minor (e.g. image/jpeg)
    {{autor}} / {{user}} uploader username

    Add a new language by appending another top-level key to translations.json and flipping LANG at the top of fotografia.py.

    Files are fetched from the Wikimedia Commons SQL replica, so this template only works on Toolforge.

  • {{Wikipedysta:WikiZEITBot/szablon}} — no-op test slot. The bot recognizes it and does nothing. Reserved for new handlers under development.

Architecture

  • bot.py — controller. Scans the category, matches the regex generated from registered handlers, parses params, calls handler.handle(...), and persists each returned PageWrite.
  • handlers.pyTemplateHandler, PaginatedHandler (chunking + main/sub envelopes), NoOpHandler, and the PageWrite dataclass.
  • podopieczni.pyMenteesHandler and all mentee-specific code (mentee fetch, user-info via API or SQL, eligibility filter, render template).
  • notifications.pyNotificationManager: tracks per-run counters, appends each run to ~/state/notifications/runs.jsonl, and when invoked with --summary reads the accumulated log, mails a digest to the operator, sends per-mentor newcomer summaries to opted-in mentors (email=tak) via MediaWiki's emailuser API, and clears the file. Error emails (send_failure) are always sent regardless of --summary. The operator digest and error mail rely on the local Exim relay (Toolforge only); the per-mentor mail goes through the wiki API. Unit-tested by test_notifications.pypython -m unittest test_notifications.py.
  • db.py — SQLite store at ~/state/bot.db (auto-created on first run). Tables: mentor_params (last seen template params per mentor), mentee_membership (per-mentor mentee roster with first_seen / last_seen), digest_meta (last sent digest timestamp). update_mentor(...) atomically reconciles the roster and returns the (added, removed) sets.
  • state.py — legacy generic JSON store; currently unused, kept for reference.
  • parsing.pyfind_template_in_text (brace-counting template locator) and parse_params (top-level | / first = split that respects nested {{...}} and [[...]]). Pure stdlib; unit-tested by test_parsing.pypython -m unittest test_parsing.py.
  • translations.json — per-language map from etykieta tokens (plik, data, …) to the canonical SQL column / format key. Add a top-level key to add a language; switch by setting LANG in fotografia.py.

Adding a new template:

  1. Subclass TemplateHandler (one-shot output) or PaginatedHandler (paginated output).
  2. Set template_name to the exact wiki template name.
  3. Implement handle(), or for paginated handlers fetch_items() + render_item().
  4. Register an instance in HANDLERS inside bot.py.

Local setup

git clone git@github.com:WikiZEIT/bot.git
cd bot
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

To upgrade later: pip install -r requirements.txt --upgrade.

Create user-config.py next to bot.py:

family = 'wikipedia'
mylang = 'pl'
usernames['wikipedia']['pl'] = 'WikiZEITBot'
password_file = 'user-password.py'

Create user-password.py from a BotPassword issued at Specjalna:HasłaBotów. The first BotPassword argument is the suffix only, not the full user@suffix login:

('WikiZEITBot', BotPassword('<suffix>', '<generated-token>'))

The BotPassword must grant Edit existing pages, Create, edit, and move pages, and High-volume editing (without the last grant pywikibot logs a 'bot' right wasn't activated warning and edits don't get the bot flag). For the per-mentor email feature (email=tak, see Email notifications) it must also grant Send email to other users (sendemail) — a BotPassword's effective rights are the intersection of the account's rights and the ticked grants, so without it the emailuser API returns "You don't have permission to send mail". In addition, the WikiZEITBot account itself must have a confirmed email address (Specjalna:Preferencje); the API refuses to send from an account without one.

Running

Configurable constants:

bot.py (controller):

Constant Meaning
DEBUG When True, the bot writes each page's output to DEBUG_DIR/<n> files instead of saving to the wiki. Flip to False for production.
DEBUG_DIR Output directory for the dry-run (default pages).
EMAIL_NOTIFICATIONS When True (and DEBUG is False), send a per-run summary email after a successful run and an urgent email with traceback on crash. Forced off while DEBUG=True.
CATEGORY Name of the monitoring category the bot scans.

podopieczni.py (mentees handler):

Constant Meaning
USE_SQL When True, fetch user info from the Wikimedia SQL replica (replica.my.cnf) and sort mentees by last-edit timestamp. Only works on Toolforge. Leave False for local testing.
LAST_EDIT_CUTOFF_DAYS In SQL mode, drop mentees whose last edit is older than this many days.
EXCLUDED_GROUPS User groups filtered out of the mentee list (default {'editor', 'sysop'}).
MenteesHandler.items_per_page Pagination size.
MenteesHandler.subpage_prefix Wikitext prepended to each subpage body.

Then:

python bot.py              # full update: re-render every monitored page, no email
python bot.py --new-only   # incremental: skip pages whose params + mentee list
                           # match the state stored in ~/state/bot.db
python bot.py --summary    # full update + email the accumulated digest, then clear the log
python bot.py --migrate    # populate ~/state/bot.db from the current wiki state
                           # without re-rendering anything (one-off bootstrap)

After deploying or upgrading the bot for the first time on a host where pages were rendered by a previous version, run python bot.py --migrate once. It walks the monitored category, fetches each mentor's current eligible mentee set, and inserts the rows into mentee_membership with a sentinel first_seen = 1970-01-01 so they don't appear as newcomers in the next digest. The following --new-only run will then be a cache hit and skip every page that genuinely hasn't changed.

--new-only and --summary compose freely — typical Toolforge layout uses --new-only hourly (append-only logging, no mail) and --summary daily (full run, send digest).

The MenteesHandler reconciles its mentee roster against ~/state/bot.db after every successful run. A subsequent --new-only run is a cache hit when the stored params match the current ones and the stored mentee set matches the current eligible set and the corresponding wiki pages exist; otherwise it re-renders and updates the DB. Each commit logs Zmiany u <mentor>: +N nowych, -M odeszło so you can see joins and departures in the job log.

The daily digest reads mentee_membership.first_seen >= digest_meta.last_digest_time and appends a "Nowi podopieczni" section listing newcomers per mentor since the previous digest.

Email notifications

On a --summary run the operator always receives the full digest at TO_ADDR (bot@wikizeit.edu.pl). In addition, any mentor whose {{Podopieczni}} template carries email=tak receives their own message listing only their newcomers since the previous digest (same as the operator summary) — never the operator digest, and never other mentors' data. The full roster is deliberately left out (a mentor may have thousands of mentees); the mail links to the mentor's page instead. Mentors without new mentees in a given digest are not contacted.

These per-mentor messages are sent through MediaWiki's emailuser API (pywikibot.User(site, mentor).send_email(...)), so the bot never handles the mentor's address — Wikipedia relays the mail to whatever address the mentor confirmed. This requires that:

  • the bot account has a confirmed e-mail address, and
  • the mentor confirmed their e-mail and left "Zezwalaj innym użytkownikom na wysyłanie do mnie e-maili" enabled.

The per-mentor mails are sent before the operator digest is composed, and every outcome is reported back in the digest under a Powiadomienia e-mail do mentorów section, e.g.:

Powiadomienia e-mail do mentorów: 2 wysłane, 1 pominięte, 1 błędne
  wysłane: Alice, Bob
  pominięte: Carol (nie przyjmuje e-maili)
  błędy: Dave (RuntimeError: ...)

so you can confirm from the summary alone that each mentor mail went out. Nothing here crashes the bot: a mentor who is not emailable (isEmailable() is False) is reported as pominięte, and any send error — including the bot account itself lacking a confirmed e-mail — is caught per mentor and reported as błędne. A failure to reach one mentor never blocks the others or the digest bookkeeping (log clearing / last_digest_time update).

The MenteesHandler writes ~/state/podopieczni/<mentor>.json after every successful run containing the current template params and a SHA-256 hash of the sorted eligible-mentee names. A subsequent --new-only run reads that file; if both params and mentees_hash match the current run, the handler returns [] and the page is skipped entirely (no wiki traffic).

Toolforge deployment

Tool: wikizeit-bot (tool home at /data/project/wikizeit-bot/).

become wikizeit-bot
git clone git@github.com:WikiZEIT/bot.git
cd bot && python3 -m venv venv && venv/bin/pip install -r requirements.txt pymysql

toolforge-jobs run wikizeit-hourly \
  --image python3.11 \
  --schedule '0 * * * *' \
  --command '/data/project/wikizeit-bot/bot/venv/bin/python /data/project/wikizeit-bot/bot/bot.py --new-only'

toolforge-jobs run wikizeit-daily \
  --image python3.11 \
  --schedule '0 0 * * *' \
  --command '/data/project/wikizeit-bot/bot/venv/bin/python /data/project/wikizeit-bot/bot/bot.py --summary'

Keep user-config.py and user-password.py in /data/project/wikizeit-bot/ (outside the repo) and pass --env PYWIKIBOT_DIR=/data/project/wikizeit-bot to the jobs command so pywikibot finds them.

Continuous deployment

.github/workflows/deploy.yaml runs on every push and pull request to master:

  1. test — installs requirements.txt on Python 3.11 (matching the Toolforge image) and runs python -m unittest test_parsing.py test_notifications.py. PYWIKIBOT_NO_USER_CONFIG=1 is set because notifications.py imports pywikibot at load time and CI has no user-config.py.
  2. deployneeds: test, so it only runs when the tests pass, and only on master. It SSHes into the Toolforge bastion and, as the tool account, pulls the latest code (git pull --ff-only) and reinstalls dependencies ($HOME/pvenv/bin/pip install -r requirements.txt). A failing test blocks the deploy entirely.

Configure these repository secrets (Settings → Secrets and variables → Actions):

Secret Value
SSH_SERVER login.toolforge.org
SSH_USER your Wikitech shell username (a maintainer of the wikizeit-bot tool)
SSH_PRIVATE_KEY a private key whose public half is on that Wikitech account
SSH_PORT 22
SSH_PATH /data/project/wikizeit-bot/bot (the repo checkout)

Both commands run as the tool account via become (in a single bash -lc so $HOME resolves to the tool's home), keeping the checkout owned by tools.wikizeit-bot. --ff-only makes the deploy fail loudly rather than create a merge commit if the checkout has diverged. Dependencies are reinstalled from requirements.txt on every deploy, so a change there is picked up automatically.

License

Copyright (C) 2026 Jakub T. Jankiewicz

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/ or LICENSE in this repository.

Releases

Packages

Contributors

Languages