feat: OMEMO по умолчанию, admin_web+BOSH, MUC-модули (vcard_muc, muc_moderation), модули из apt в modules/, scripts/omemo_check.py, backup.sh на Яндекс.Диск

- index.html: omemo_default=true (XEP-0384, встроен в Converse v14)
- prosody.cfg.lua: admin_web + bosh (глобальные), MUC: vcard_muc (XEP-0153) + muc_moderation (XEP-0425)
- modules/: mod_admin_web (+www_files), mod_http_upload_external, mod_muc_moderation, mod_vcard_muc — из prosody-modules (apt)
- scripts/omemo_check.py — проверка OMEMO-бандлов
- backup.sh — ежедневный бэкап проекта (data/config/certs/modules/webchat/доки) на /mnt/yandex-disk, ротация 7/30 дней
- STATUS/WALKTHROUGH: OMEMO, admin_web, HTTP Upload итоги
This commit is contained in:
2026-08-29 07:33:14 +00:00
parent 0695ddef75
commit 527584281d
19 changed files with 1878 additions and 5 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/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 <id>);
- по желанию чистит список (--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())