#!/usr/bin/env python3 """Проверка OMEMO-связки на сервере nixg.ru (XEP-0384, PEP). Что делает: - логинится admin@nixg.ru по WebSocket (wss://xmpp.nixg.ru/xmpp-websocket); - читает OMEMO devicelist (eu.siacs.conversations.axolotl.devicelist); - по желанию публикует новое устройство (--publish ); - по желанию чистит список (--clean, оставляет только реальные устройства). Использование: PASSWORD=... /opt/icq/.venv/bin/python /opt/icq/scripts/omemo_check.py [--publish 7777] [--clean] JID=admin@nixg.ru (по умолчанию) — можно переопределить env JID. Зависимости: venv /opt/icq/.venv (slixmpp 1.17). Служебные находки (проверено 2026-08-29): - slixmpp 1.17: connect((url,)) сам управляет циклом, process() НЕТ; - плагины регистрировать явно: register_plugin('xep_0060'); - publish payload = XML-элемент (slixmpp.xmlstream.ET), не строка; - get_items через WS иногда возвращает пусто — смотреть запись на диске data/nixg%2eru/pep_eu%2esiacs%2econversations%2eaxolotl%2edevicelist/admin.list """ import asyncio, os, ssl, sys sys.path.insert(0, '/opt/icq/.venv/lib/python3.12/site-packages') import slixmpp from slixmpp.exceptions import IqError, IqTimeout from slixmpp.xmlstream import ET JID = os.environ.get('JID', 'admin@nixg.ru') PASSWORD = os.environ.get('PASSWORD', '') if not PASSWORD: print('ОШИБКА: задайте PASSWORD (env)'); sys.exit(2) WS_URL = 'wss://xmpp.nixg.ru/xmpp-websocket' NS_DEVICELIST = 'eu.siacs.conversations.axolotl.devicelist' NS_OMEMO = 'eu.siacs.conversations.axolotl' ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE class OmemoCheck(slixmpp.ClientXMPP): def __init__(self, do_publish=None, do_clean=False): super().__init__(JID, PASSWORD) self.register_plugin('xep_0060') self.do_publish = do_publish self.do_clean = do_clean self.add_event_handler('session_start', self.go) async def read_devicelist(self): try: iq = await self['xep_0060'].get_items(jid=JID, node=NS_DEVICELIST, max_items='') its = iq['pubsub']['items']['item'] ids = [] for it in (its or []): for dev in it.xml.iter('{%s}device' % NS_OMEMO): ids.append(dev.get('id')) print('[read] devicelist:', ids or '(пусто — смотри на диске, см. доки)') return ids except IqError as ex: print('[read] IqError:', ex.iq['error']['condition']); return None except Exception as ex: print('[read]', type(ex).__name__, ex); return None async def go(self, event): self.send_presence(); await self.get_roster() print('[session] OK') ids = await self.read_devicelist() if self.do_publish: lst = ET.Element('{%s}list' % NS_OMEMO) for d in (ids or []): ET.SubElement(lst, '{%s}device' % NS_OMEMO, {'id': d}) ET.SubElement(lst, '{%s}device' % NS_OMEMO, {'id': self.do_publish}) try: await self['xep_0060'].publish(jid=JID, node=NS_DEVICELIST, id='current', payload=lst) print('[publish]', self.do_publish, 'ACCEPTED') except IqError as ex: print('[publish] IqError:', ex.iq['error']['condition']) except IqTimeout: print('[publish] timeout') except Exception as ex: print('[publish]', type(ex).__name__, ex) if self.do_clean: keep = [d for d in (ids or []) if d != self.do_publish] if self.do_publish else (ids or []) # без явного списка чистить неоткуда — просто ре-публикуем текущий lst = ET.Element('{%s}list' % NS_OMEMO) for d in keep: ET.SubElement(lst, '{%s}device' % NS_OMEMO, {'id': d}) try: await self['xep_0060'].publish(jid=JID, node=NS_DEVICELIST, id='current', payload=lst) print('[clean] devicelist ->', keep, 'ACCEPTED') except Exception as ex: print('[clean]', type(ex).__name__, ex) await asyncio.sleep(1) self.disconnect() async def main(): do_publish = None do_clean = False args = sys.argv[1:] if '--publish' in args: do_publish = args[args.index('--publish') + 1] if '--clean' in args: do_clean = True bot = OmemoCheck(do_publish=do_publish, do_clean=do_clean) bot.ssl_context = ctx try: fut = bot.connect((WS_URL,)) await asyncio.wait_for(fut, timeout=25) await asyncio.sleep(8) try: bot.disconnect() except Exception: pass except Exception as ex: print('connect error:', ex) if __name__ == '__main__': asyncio.run(main())