2021-07-15 19:29:48 +00:00
|
|
|
#!/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 <SupportedXep> 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 <SupportedXep> 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
|
2021-07-15 19:58:30 +00:00
|
|
|
258, # Security Labels in XMPP
|
|
|
|
280, # Message Carbons
|
2021-07-15 19:29:48 +00:00
|
|
|
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)
|