diff --git a/README.md b/README.md index 47ee475..e8039fa 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,30 @@ $ ls -la /tmp/restore/var/lib/mediawiki-uploads/ $ umount /tmp/restore/ ``` +## Searching for wiki users (wiki admins only) + +The `wiki-user-search` tool allows administrators to search for MediaWiki users +by username, email, or real name. Since it connects to PostgreSQL, it must be +run as the `postgres` user: + +```bash +# Search by username +$ sudo -u postgres wiki-user-search "admin" + +# Search by email domain +$ sudo -u postgres wiki-user-search "@example.com" + +# Search by real name +$ sudo -u postgres wiki-user-search "John Smith" + +# Limit results (default is 10) +$ sudo -u postgres wiki-user-search "test" -l 20 +``` + +The tool displays results in a table format showing Username, Email, and Real +Name. It uses fuzzy matching with case-insensitive search and prioritizes exact +matches over partial matches. + ## Applying terraform Updating hetzner ssh keys: diff --git a/flake.nix b/flake.nix index dacfb6f..f5121cb 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,7 @@ ... }: { + packages = import ./pkgs { inherit pkgs; }; checks = let diff --git a/modules/nixos-wiki/default.nix b/modules/nixos-wiki/default.nix index 3f988b1..e25d5f5 100644 --- a/modules/nixos-wiki/default.nix +++ b/modules/nixos-wiki/default.nix @@ -50,6 +50,10 @@ in }; config = { + environment.systemPackages = [ + (pkgs.python3.pkgs.callPackage ../../pkgs/wiki-user-search { }) + ]; + services.mediawiki = { name = "NixOS Wiki"; enable = true; diff --git a/pkgs/default.nix b/pkgs/default.nix new file mode 100644 index 0000000..bebf6d6 --- /dev/null +++ b/pkgs/default.nix @@ -0,0 +1,5 @@ +{ pkgs }: + +{ + wiki-user-search = pkgs.python3.pkgs.callPackage ./wiki-user-search { }; +} diff --git a/pkgs/wiki-user-search/default.nix b/pkgs/wiki-user-search/default.nix new file mode 100644 index 0000000..042a68a --- /dev/null +++ b/pkgs/wiki-user-search/default.nix @@ -0,0 +1,32 @@ +{ + lib, + psycopg2, + buildPythonApplication, +}: + +buildPythonApplication { + pname = "wiki-user-search"; + version = "1.0.0"; + + src = ./.; + + format = "other"; + + propagatedBuildInputs = [ psycopg2 ]; + + installPhase = '' + runHook preInstall + + install -Dm755 wiki-user-search.py $out/bin/wiki-user-search + + runHook postInstall + ''; + + meta = with lib; { + description = "MediaWiki user search tool with fuzzy matching"; + homepage = "https://github.com/NixOS/nixos-wiki-infra"; + license = licenses.mit; + maintainers = [ ]; + mainProgram = "wiki-user-search"; + }; +} diff --git a/pkgs/wiki-user-search/wiki-user-search.py b/pkgs/wiki-user-search/wiki-user-search.py new file mode 100644 index 0000000..b9eca66 --- /dev/null +++ b/pkgs/wiki-user-search/wiki-user-search.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +""" +MediaWiki User Search Tool +Fuzzy search for users by username, real name, or email +""" + +import argparse +import psycopg2 +import sys +from typing import List, Tuple + + +def search_users(search_term: str, limit: int = 10) -> List[Tuple[str, str, str]]: + """ + Search for users using fuzzy matching on username, real name, or email. + Returns list of (username, email, real_name) tuples. + """ + # Connect via Unix socket + try: + conn = psycopg2.connect( + host="/run/postgresql", dbname="mediawiki", user="postgres" + ) + except psycopg2.Error as e: + print(f"Error connecting to database: {e}", file=sys.stderr) + sys.exit(1) + + cur = conn.cursor() + + # Use PostgreSQL's ILIKE for case-insensitive pattern matching + query = """ + SELECT DISTINCT + user_name, + user_email, + user_real_name, + CASE + WHEN LOWER(user_name) = LOWER(%s) THEN 1 + WHEN LOWER(user_email) = LOWER(%s) THEN 1 + WHEN LOWER(user_real_name) = LOWER(%s) THEN 1 + WHEN LOWER(user_name) LIKE LOWER(%s) THEN 2 + WHEN LOWER(user_email) LIKE LOWER(%s) THEN 2 + WHEN LOWER(user_real_name) LIKE LOWER(%s) THEN 2 + ELSE 3 + END as match_score + FROM mediawiki."user" + WHERE + user_name ILIKE %s + OR user_email ILIKE %s + OR user_real_name ILIKE %s + ORDER BY match_score, user_name + LIMIT %s + """ + + # Create search pattern with wildcards + search_pattern = f"%{search_term}%" + + # Execute query with all parameters + cur.execute( + query, + ( + search_term, + search_term, + search_term, # Exact match checks + search_pattern, + search_pattern, + search_pattern, # Pattern match checks + search_pattern, + search_pattern, + search_pattern, # WHERE clause + limit, + ), + ) + + results = [] + for row in cur.fetchall(): + username, email, real_name, _ = row + results.append((username or "", email or "", real_name or "")) + + cur.close() + conn.close() + return results + + +def print_table(results: List[Tuple[str, str, str]]) -> None: + """Print search results as a formatted table.""" + if not results: + print("No users found matching the search term.") + return + + # Calculate column widths + username_width = max(len("Username"), max(len(r[0]) for r in results)) + email_width = max(len("Email"), max(len(r[1]) for r in results)) + real_name_width = max(len("Real Name"), max(len(r[2]) for r in results)) + + # Print header + print( + f"{'Username':<{username_width}} | {'Email':<{email_width}} | {'Real Name':<{real_name_width}}" + ) + print(f"{'-' * username_width}-+-{'-' * email_width}-+-{'-' * real_name_width}") + + # Print rows + for username, email, real_name in results: + print( + f"{username:<{username_width}} | {email:<{email_width}} | {real_name:<{real_name_width}}" + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Search MediaWiki users by username, email, or real name" + ) + parser.add_argument( + "search_term", help="Search term to match against username, email, or real name" + ) + parser.add_argument( + "--limit", + "-l", + type=int, + default=10, + help="Maximum number of results (default: 10)", + ) + + args = parser.parse_args() + + # Search for users + results = search_users(args.search_term, args.limit) + + # Print results as table + print_table(results) + + +if __name__ == "__main__": + main()