#!/usr/bin/env python3 import json import xml.etree.ElementTree import requests import colorama import prettytable CLIENTS_URL = 'https://raw.githubusercontent.com/xsf/xmpp.org/master/data/clients.json' XMPP_DOAP_NS = 'https://linkmauve.fr/ns/xmpp-doap#' # the # is mandatory # XPaths for in DOAPs XPATH_DOAP_SUPPORTEDXEP = f'.//{{{XMPP_DOAP_NS}}}SupportedXep' XPATH_DOAP_XEP = f'.//{{{XMPP_DOAP_NS}}}xep' XPATH_DOAP_XEP_VERSION = f'.//{{{XMPP_DOAP_NS}}}version' XPATH_DOAP_XEP_STATUS = f'.//{{{XMPP_DOAP_NS}}}status' XPATH_RDF_RESOURCE = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}resource' # XPaths for XEP definitions (at xmpp.org/extensions/) XPATH_XEP_TITLE = './header/title' XPATH_XEP_STATUS = './header/status' SUPPORT_INDICATORS = { 'planned': colorama.Fore.RED + '⧗' + colorama.Fore.RESET, 'partial': colorama.Fore.YELLOW + '✓' + colorama.Fore.RESET, 'complete': colorama.Fore.GREEN + '✓' + colorama.Fore.RESET, 'removed': colorama.Fore.RED + '🕇' + colorama.Fore.RESET, 'wontfix': colorama.Fore.RED + '☠' + colorama.Fore.RESET, 'SUPPORTED_UNKNOWN': colorama.Fore.YELLOW + '?' + colorama.Fore.RESET, # for XEPs mentioned in a tag but where the status isn't defined 'NOT_MENTIONED': colorama.Fore.RED + '✗' + colorama.Fore.RESET, # for XEPs that are not mentioned in clients' DOAP data } SUPPORT_LEGEND = { 'NOT_MENTIONED': 'No support', 'planned': 'Planned support', 'partial': 'Partial support', 'SUPPORTED_UNKNOWN': 'Supported, but status not specified', 'complete': 'Complete support', 'removed': 'Removed support', 'wontfix': "Client will never support this XEP", } XEP_LIST = [ 30, # Service Discovery 45, # Multi-User Chat 166, # Jingle 167, # Jingle RTP Sessions 174, # Serverless Messaging #176, # Jingle ICE-UDP Transport Method #177, # Jingle Raw UDP Transport Method 198, # Stream Management 199, # XMPP Ping 258, # Security Labels in XMPP 280, # Message Carbons 352, # Client State Indication 357, # Push Notifications 363, # HTTP File Upload 368, # SRV records for XMPP-over-TLS 373, # OpenPGP for XMPP 384, # OMEMO encryption 449, # Stickers 454, # OMEMO media sharing ] def acceptable_status(status): if status >= 200 and status <= 400: # status ranges 200 (OK) and 300 (redirects) are okay return True return False def xep_format_number(xep_number): return f"{xep_number:04d}" def xep_get_info(xep_number): xep_number = xep_format_number(xep_number) req = requests.get(f'https://xmpp.org/extensions/xep-{xep_number}.xml') xep_tree = xml.etree.ElementTree.fromstring(req.text) xep_title = xep_tree.find(XPATH_XEP_TITLE).text xep_status = xep_tree.find(XPATH_XEP_STATUS).text return { 'number': xep_number, 'title': xep_title, 'status': xep_status, } if __name__ == '__main__': clients_req = requests.get(CLIENTS_URL) if not acceptable_status(clients_req.status_code): quit(f"Encountered error when loading client definitions from '{CLIENTS_URL}'!") else: colorama.init() # print iconography legend for key, info in SUPPORT_LEGEND.items(): print(SUPPORT_INDICATORS[key] + ': ' + info) print("\rWorking…", end='') table_data = {} xeps_covered = set() clients = json.loads(clients_req.text) for client in clients: if client['doap']: doap_req = requests.get(client['doap']) if not acceptable_status(doap_req.status_code): print(f"\rEncountered error when loading client DOAP from '{client['doap']}'!", end='') else: doap = xml.etree.ElementTree.fromstring(doap_req.text) client_data = {} xep_entries = doap.findall(XPATH_DOAP_SUPPORTEDXEP) for xep_entry in xep_entries: xep_resource = xep_entry.find(XPATH_DOAP_XEP).attrib[XPATH_RDF_RESOURCE] # the following abomination extracts the XEP number # from an URL Like https://xmpp.org/extensions/xep-0030.html xep_number = int(xep_resource.split('/')[-1].split('.')[0].split('-')[-1]) xep_status = xep_entry.find(XPATH_DOAP_XEP_STATUS) if not xep_status is None: xep_status = xep_status.text xep_version = xep_entry.find(XPATH_DOAP_XEP_VERSION) if not xep_version is None: xep_version = xep_version.text xeps_covered.add(xep_number) client_data[xep_number] = {'status': xep_status, 'version': xep_version} table_data[client['name']] = client_data table = prettytable.PrettyTable() field_names = ['XEP'] for name in table_data: field_names.append(name) table.field_names = field_names # doing append directly on PrettyTape.field_names majorly fucks shit up table.align['XEP'] = 'l' xep_list = XEP_LIST if len(xep_list) == 0: xep_list = sorted(xeps_covered) for xep_number in xep_list: xep_info = xep_get_info(xep_number) row = [f"{xep_info['number']}: {xep_info['title']}({xep_info['status']})"] for client_name, client_info in table_data.items(): if not xep_number in client_info: row.append(SUPPORT_INDICATORS['NOT_MENTIONED']) else: if client_info[xep_number]['status'] is None: row.append(SUPPORT_INDICATORS['SUPPORTED_UNKNOWN']) else: row.append(SUPPORT_INDICATORS[client_info[xep_number]['status']]) table.add_row(row) print("\r", end='') # remove 'Working…' print(table)