From: Michael Tremer Date: Wed, 31 Dec 2025 13:16:41 +0000 (+0000) Subject: lists: Add command to show the history X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=934c588f0729f3c0d147373649549129ee812a35;p=dbl.git lists: Add command to show the history Signed-off-by: Michael Tremer --- diff --git a/src/dnsbl/lists.py b/src/dnsbl/lists.py index 6edcb2b..c8635c7 100644 --- a/src/dnsbl/lists.py +++ b/src/dnsbl/lists.py @@ -22,6 +22,8 @@ import datetime import functools import io import logging +import sqlalchemy.dialects.postgresql +import sqlalchemy.orm import sqlmodel import typing @@ -409,3 +411,110 @@ class List(sqlmodel.SQLModel, database.BackendMixin, table=True): # Pending Reports pending_reports : int = 0 + + # History + + def get_history(self, limit=None): + """ + Fetches the history of the list + """ + # Collect all domains that have been added + domains_added = ( + sqlmodel + .select( + domains.Domain.added_at.label("ts"), + + # Aggregate all domains + sqlmodel.func.array_agg( + sqlalchemy.dialects.postgresql.aggregate_order_by( + domains.Domain.name, + domains.Domain.name.asc(), + ), + ).label("names"), + ) + .where( + domains.Domain.list == self, + ) + .order_by( + domains.Domain.added_at.desc(), + ) + .group_by( + domains.Domain.added_at, + ) + .limit(limit) + .cte("domains_added") + ) + + # Collect all domains that have been removed + domains_removed = ( + sqlmodel + .select( + domains.Domain.removed_at.label("ts"), + + # Aggregate all domains + sqlmodel.func.array_agg( + sqlalchemy.dialects.postgresql.aggregate_order_by( + domains.Domain.name, + domains.Domain.name.asc(), + ), + ).label("names"), + ) + .where( + domains.Domain.list == self, + domains.Domain.removed_at != None, + ) + .order_by( + domains.Domain.removed_at.desc(), + ) + .group_by( + domains.Domain.removed_at, + ) + .limit(limit) + .cte("domains_removed") + ) + + # Union timestamps + timestamps = ( + sqlmodel + .union( + sqlmodel.select( + domains_added.c.ts, + ), + sqlmodel.select( + domains_removed.c.ts, + ), + ) + .cte("timestamps") + ) + + stmt = ( + sqlmodel + .select( + timestamps.c.ts, + + # Fetch all added domains + sqlmodel.func.coalesce( + domains_added.c.names, + sqlmodel.text("'{}'"), + ).label("domains_added"), + + # Fetch all removed domains + sqlmodel.func.coalesce( + domains_removed.c.names, + sqlmodel.text("'{}'"), + ).label("domains_removed"), + ) + .outerjoin( + domains_added, + domains_added.c.ts == timestamps.c.ts, + ) + .outerjoin( + domains_removed, + domains_removed.c.ts == timestamps.c.ts, + ) + .order_by( + timestamps.c.ts.asc(), + ) + ) + + return self.backend.db.select(stmt) diff --git a/src/scripts/dnsbl.in b/src/scripts/dnsbl.in index 57229a7..911cc15 100644 --- a/src/scripts/dnsbl.in +++ b/src/scripts/dnsbl.in @@ -20,6 +20,7 @@ ############################################################################### import argparse +import babel.dates import babel.numbers import dnsbl import dnsbl.checker @@ -154,6 +155,15 @@ class CLI(object): analyze.add_argument("list", help=_("The name of the list")) analyze.set_defaults(func=self.__analyze) + # history + history = subparsers.add_parser("history", + help=_("Shows the latest changes of a list")) + history.add_argument("list", + help=_("The name of the list")) + history.add_argument("--limit", type=int, default=10, + help=_("Only show a few changesets")) + history.set_defaults(func=self.__history) + # check-domains check_domains = subparsers.add_parser("check-domains", help=_("Checks if domains are alive")) @@ -511,6 +521,35 @@ class CLI(object): # Print the table self.console.print(table) + def __history(self, backend, args): + """ + Shows the history of a list + """ + # Fetch the list + list = backend.lists.get_by_slug(args.list) + + # Fetch the history + history = list.get_history(limit=args.limit) + + # Create a new table + table = rich.table.Table(title=_("History of %s") % list) + + # Add the header + table.add_column(_("Timestamp")) + table.add_column(_("Domains Added")) + table.add_column(_("Domains Removed")) + + # Add the history + for events in history: + table.add_row( + babel.dates.format_datetime(events.ts), + "\n".join(events.domains_added), + "\n".join(events.domains_removed), + ) + + # Print the table + self.console.print(table) + def __check_domains(self, backend, args): """ Runs the checker over all domains