diff --git a/scripts/check-new-xeps.py b/scripts/check-new-xeps.py new file mode 100755 index 00000000..a62d6a09 --- /dev/null +++ b/scripts/check-new-xeps.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +import os +import re +import sys +import urllib.request +import xml.etree.ElementTree as ET +from itertools import zip_longest +from typing import Dict, List, Optional + + +# Namespaces in DOAP +NS = { + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "foaf": "http://xmlns.com/foaf/0.1/", + "doap": "http://usefulinc.com/ns/doap#", + "xmpp": "https://linkmauve.fr/ns/xmpp-doap#", + "schema": "https://schema.org/", +} + + +def compare_versions(v1: str, v2: str) -> int: + """ + Compare two version strings. + Returns 1 if v2 > v1, -1 if v1 > v2, 0 if equal. + """ + def parse_v(v: str) -> List[int]: + return [int(x) for x in re.split(r"[^0-9]+", v) if x] + + parts1 = parse_v(v1) + parts2 = parse_v(v2) + + for p1, p2 in zip_longest(parts1, parts2, fillvalue=0): + if p2 > p1: + return 1 + if p1 > p2: + return -1 + return 0 + + +def find_doap() -> Optional[str]: + """Try to find profanity.doap in current or parent directory.""" + candidates = ["profanity.doap", "../profanity.doap"] + for c in candidates: + if os.path.exists(c): + return c + return None + + +def get_implemented_xeps(doap_path: str) -> Dict[str, str]: + """Parse DOAP file and return a map of XEP number to version.""" + implemented_xeps: Dict[str, str] = {} + try: + tree = ET.parse(doap_path) + root = tree.getroot() + + for implements in root.findall(".//doap:implements", NS): + supported_xep = implements.find(".//xmpp:SupportedXep", NS) + if supported_xep is not None: + xep_res = supported_xep.find(".//xmpp:xep", NS) + version_elem = supported_xep.find(".//xmpp:version", NS) + + if xep_res is not None and version_elem is not None: + resource = xep_res.attrib.get(f"{{{NS['rdf']}}}resource", "") + match = re.search(r"xep-(\d+)\.html", resource) + if match: + xep_num = match.group(1) + current_version = version_elem.text.strip() if version_elem.text else "" + implemented_xeps[xep_num] = current_version + except (ET.ParseError, PermissionError) as e: + print(f"Error reading {doap_path}: {e}") + sys.exit(1) + + return implemented_xeps + + +def check_xeps() -> None: + """Main logic for checking XEP updates.""" + doap_path = find_doap() + if not doap_path: + print("Error: Could not find DOAP file.") + sys.exit(1) + + implemented_xeps = get_implemented_xeps(doap_path) + if not implemented_xeps: + print("No XEPs found in DOAP file.") + return + + print(f"XEPs tracked: {len(implemented_xeps)}") + + try: + url = "https://xmpp.org/extensions/xeplist.xml" + with urllib.request.urlopen(url, timeout=15) as response: + xeplist_xml = response.read() + except Exception as e: + print(f"Error fetching xeplist.xml: {e}") + return + + try: + xeplist_tree = ET.fromstring(xeplist_xml) + except ET.ParseError as e: + print(f"Error parsing xeplist.xml: {e}") + return + + print(f"\n{'XEP':<10} | {'Name':<35} | {'Ours':<8} | {'Latest':<8}") + print("-" * 80) + + updates_found = 0 + for xep_node in xeplist_tree.findall("xep"): + number_node = xep_node.find("number") + title_node = xep_node.find("title") + last_revision_node = xep_node.find("last-revision") + + if number_node is None or title_node is None or last_revision_node is None: + continue + + number = (number_node.text or "0").zfill(4) + name = title_node.text or "Unknown" + version_node = last_revision_node.find("version") + + if version_node is None or version_node.text is None: + continue + + latest_version = version_node.text + + if number in implemented_xeps: + old_version = implemented_xeps[number] + if compare_versions(old_version, latest_version) > 0: + print(f"XEP-{number:<4} | {name[:35]:<35} | {old_version:<8} | {latest_version:<8}") + updates_found += 1 + + if updates_found == 0: + print("All tracked XEPs are up to date.") + + +if __name__ == "__main__": + check_xeps()