diff --git a/README.md b/README.md index 5285b81..51bdee2 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,40 @@ - **Caddy** (vps02, host-network) — TLS-терминатор: chat.nixg.ru, xmpp.nixg.ru, upload.nixg.ru - **Let's Encrypt** — сертификаты для XMPP (c2s/s2s) и веб - **WireGuard** — туннель bigbox (10.8.0.2) ↔ vps02 (10.8.0.4) +- **Slidge/slidgram** (Docker) — мост XMPP↔Telegram (внешний компонент, XEP-0114) + +## Функциональность (XEP-модули) + +| Возможность | XEP | Модуль | Статус | +|---|---|---|---| +| MUC (групповые чаты) | XEP-0045 | Component `conference.nixg.ru` (mod_muc) | ✅ | +| PubSub (подписки, ленты) | XEP-0060 | mod_pubsub | ✅ | +| HTTP Upload (файлы) | XEP-0363 | Component `upload.nixg.ru` (mod_http_upload, community) | ✅ | +| MAM (история) | XEP-0313 | mod_mam (+mod_muc_mam) | ✅ | +| Carbons (все устройства) | XEP-0280 | mod_carbons | ✅ | +| OMEMO / OTR (сквозное шифрование) | XEP-0384 | PEP (mod_pep) для хранения ключей; серверный mod_omemo в 0.11 отсутствует — шифрование клиент-сайд (Gajim/Dino/Conversations) | ⚠️ клиент-сайд | +| WebRTC (звонки) | XEP-0166/0343 | клиент-сайд (Jingle); серверного модуля нет | ⚠️ клиент-сайд | +| Bookmarks (избранные) | XEP-0402 | через PEP/pubsub node `storage:bookmarks` (отдельного mod_bookmarks в 0.11 нет) | ✅ через PEP | +| Privilege (roster sync для мостов) | XEP-0356 | mod_privilege (community) | ✅ | +| PEP (аватары, OMEMO, отметки) | XEP-0163 | mod_pep | ✅ | + +## Мост XMPP↔Telegram (Slidge/slidgram) + +- Внешний компонент **telegram.nixg.ru** (XEP-0114), порт 5347 (внутри docker-сети). +- Образ `slidgram-proxy:latest` = codeberg.org/slidge/slidgram + SOCKS5-патч (env `SLIDGRAM_PROXY`). +- **Трафик Telegram идёт через SOCKS5 172.27.0.1:1080** (SSH-туннель VPS01, вне РФ) — работает при блокировке Telegram в РФ. +- Данные сессий: `./slidgram/data/` (том, uid 10000). +- Регистрация TG-аккаунта: из XMPP-клиента написать «register» на `telegram.nixg.ru`. +- Офлайн-доки: `docs/slidgram/` (копия slidge.im). ## Состав (после клонирования) ``` /opt/icq ├── config/ # prosody.cfg.lua (+ mounts в контейнер) ├── webchat/ # nginx: index.html + nginx.conf (dist скачивается отдельно) -├── modules/ # community-модули Prosody (mod_http_upload) +├── modules/ # community-модули Prosody (mod_http_upload, mod_privilege, ...) +├── slidgram/ # Dockerfile + патчи для образа slidgram-proxy (SOCKS5) +├── docs/slidgram/ # офлайн-копия документации slidgram ├── docker-compose.yml ├── STATUS.md # точка входа: текущее состояние, задачи ├── PRD.md # требования/архитектура diff --git a/STATUS.md b/STATUS.md index 869070a..79e2b3a 100644 --- a/STATUS.md +++ b/STATUS.md @@ -106,7 +106,8 @@ chromium --headless --no-sandbox --disable-gpu --enable-logging=stderr --virtual - TXT-записи `_acme-challenge.nixg.ru` и `_acme-challenge.xmpp.nixg.ru` в панели Jino (вручную) - Сертификат: /etc/letsencrypt/live/nixg.ru/ → скопирован в /opt/icq/certs/nixg.ru.{crt,key} - Проверено снаружи: c2s 5222 и s2s 5269 отдают LE (issuer=Let's Encrypt, SAN nixg.ru+xmpp.nixg.ru), TLS 1.3 - - Истекает 2026-11-26; certbot настроил автопродление — НО при dns-01 вручную автопродление НЕ сработает без повторного добавления TXT! Продление: `sudo certbot renew --manual-auth-hook /bin/true --manual-cleanup-hook /bin/true` (после добавления TXT). TODO: настроить напоминание/скрипт. + - Истекает 2026-11-26; certbot настроил автопродление — НО при dns-01 вручную автопродление НЕ сработает без повторного добавления TXT! Продление: `sudo certbot renew --manual-auth-hook /bin/true --manual-cleanup-hook /bin/true` (после добавления TXT). + - ✅ Напоминание НАСТРОЕНО как ТИХИЙ watchdog (2026-08-29): Hermes cron-джоба ca2a23a34905, ежедневно 10:00 UTC, no_agent, доставка в Telegram (DM). Скрипт /opt/hermes/.hermes/scripts/check_icq_cert.sh (THRESHOLD=45 дн.): пока до истечения > 45 дней — молчит (пустой stdout, ничего не шлётся); при <= 45 дней шлёт в TG инструкцию продления (TXT в Jino → certbot renew ×2 → копия в certs/ → restart prosody). Первое ожидаемое срабатывание ≈ 12.10.2026 (истечение 26.11). - [x] **HTTP Upload (XEP-0363) — ВЫПОЛНЕНО ✅ (2026-08-28)** - Модуль: `mod_http_upload` из Ubuntu-пакета `prosody-modules` (apt) → скопирован в ./modules/ - Prosody: `Component "upload.nixg.ru" "http_upload"`, лимит 10MB, хранение 7 дней, require_authentication, @@ -147,7 +148,21 @@ chromium --headless --no-sandbox --disable-gpu --enable-logging=stderr --virtual Веб-админки в Prosody 0.11 нет в коробке — при желании ставить отдельный UI. Функционально не блокер. - Ошибки «http_upload MUST happen with TLS» в prosody.err — ИСТОРИЧЕСКИЕ (28 авг 20:01, до настройки LE-серта); за рестарт 29 авг ошибок нет. -- [ ] Мосты mautrix (Telegram/WhatsApp) — Этап 4; нужен external component (mod_component). +- [x] **Мост XMPP↔Telegram (Slidge/slidgram) — ПОДКЛЮЧЁН (2026-08-29)** + - Prosody: component_ports 5347 + Component "telegram.nixg.ru" с component_secret (ВНИМАНИЕ: в 0.11 + секрет задаётся в секции Component через component_secret=, глобальная таблица component_secrets + НЕ читается — была засада not-authorized, решено). + - Образ: кастомный slidgram-proxy:latest = codeberg.org/slidge/slidgram:latest + патчи telegram.py/gateway.py + (прокидывают SOCKS5 из env SLIDGRAM_PROXY в Pyrogram) + pysocks (уже есть в образе, модуль socks). + - docker-compose: сервис slidgram (icq-slidgram), env SLIDGE_JID/SECRET/SERVER/PORT + SLIDGRAM_PROXY=socks5://172.27.0.1:1080 + - Трафик Telegram → SOCKS5 172.27.0.1:1080 (SSH-туннель VPS01, TELEGRAM_PROXY) — проверено: SOCKS5→MTProto DC1 OK. + - Статус: компонент аутентифицирован в Prosody, Slidge started, upload discovery OK. + - ✅ **mod_privilege (XEP-0356)** добавлен (2026-08-29): community-модуль из hg.prosody.im → ./modules/mod_privilege.lua, + включён в modules_enabled + privileged_entities в VirtualHost для telegram.nixg.ru (roster sync, legacy carbons). + - ✅ **Аудит XEP-функций** (2026-08-29) → **XEP-AUDIT.md**: MUC/PubSub/HTTP Upload/MAM/Carbons/PEP ✅; + OMEMO/WebRTC — клиент-сайд (норма для 0.11); Bookmarks — через PEP; mod_pubsub включён в modules_enabled. + - ОЖИДАЕТ: регистрацию TG-аккаунта (admin@nixg.ru → команда register на telegram.nixg.ru) + api_id/api_hash. +- [ ] Мосты mautrix (WhatsApp) — Этап 4; нужен external component (mod_component). [Telegram-мост уже на Slidge] - [ ] Push-уведомления (APNs/FCM) — ОТЛОЖЕН (нет особой потребности; см. WALKTHROUGH раздел 10.4 «Варианты»). Пробовал (2026-08-29): mod_unified_push из apt-prosody-modules НЕ подходит — требует util.jwt (API 0.12), а контейнер на 0.11.9. Для реализации нужно (любое из): diff --git a/XEP-AUDIT.md b/XEP-AUDIT.md new file mode 100644 index 0000000..51d4144 --- /dev/null +++ b/XEP-AUDIT.md @@ -0,0 +1,46 @@ +# Аудит XEP-функциональности сервера nixg.ru (Prosody 0.11.9) + +Дата: 2026-08-29. Проверено фактически: по конфигу `config/prosody.cfg.lua`, +загруженным модулям в контейнере (`docker exec icq-prosody ls ...`) и логам +(компонент telegram.nixg.ru аутентифицирован, MAM активен). + +Легенда: ✅ работает · ⚠️ частично/клиент-сайд · ❌ не реализовано + +## Основные модули (XEP) + +| Возможность | XEP | Как реализовано | Статус | +|---|---|---|---| +| **MUC** (групповые чаты) | XEP-0045 | Component `conference.nixg.ru` (mod_muc); + mod_vcard_muc (аватары комнат), mod_muc_moderation (XEP-0425), max_history 200 | ✅ | +| **PubSub** (подписки/ленты) | XEP-0060 | mod_pubsub (stock, включён 2026-08-29) | ✅ | +| **HTTP Upload** (файлы) | XEP-0363 | Component `upload.nixg.ru` (mod_http_upload, community), внешний URL https://upload.nixg.ru, лимит 10 MB, TTL 7 дней | ✅ | +| **MAM** (история) | XEP-0313 | mod_mam (в modules_enabled) + mod_muc_mam (история комнат) | ✅ | +| **Carbons** (все устройства) | XEP-0280 | mod_carbons (в modules_enabled) | ✅ | +| **OMEMO / OTR** | XEP-0384 / XEP-0363 | mod_pep (хранение ключей/бандлов OMEMO). Серверного mod_omemo в Prosody 0.11 нет (и в prosody-modules его нет) — шифрование **клиент-сайд** (Gajim/Dino/Conversations/Monal работают через PEP). | ⚠️ клиент-сайд | +| **WebRTC** (звонки) | XEP-0166 (Jingle) / XEP-0343 | Jingle — клиент-сайд; серверного модуля нет. Prosody не терминатор медиа — для звонков клиент↔клиент достаточно канала сигнализации (c2s/websocket уже есть). | ⚠️ клиент-сайд | +| **Bookmarks** (избранное) | XEP-0402 / XEP-0048 | Через PEP/pubsub node `storage:bookmarks` (mod_pep). Отдельного mod_bookmarks в 0.11 нет; mod_default_bookmarks (community) — по желанию. | ✅ через PEP | + +## Связанное (мосты и сервисы) + +| Возможность | XEP | Как реализовано | Статус | +|---|---|---|---| +| **External component** (мост Telegram) | XEP-0114 | Component `telegram.nixg.ru`, порт 5347 (внутри docker-сети), секрет в `component_secret` секции Component | ✅ | +| **Privileged Entity** (roster sync для мостов) | XEP-0356 | mod_privilege (community, добавлен 2026-08-29) + `privileged_entities` в VirtualHost для telegram.nixg.ru | ✅ | +| **PEP** (аватары, OMEMO, отметки) | XEP-0163 | mod_pep (в modules_enabled) | ✅ | +| **In-band registration** | XEP-0077 | mod_register (allow_registration=true) | ✅ | +| **WebSocket / BOSH** | XEP-0306 / XEP-0124 | mod_websocket, mod_bosh (порт 5280) | ✅ | +| **Stream Management** | XEP-0198 | mod_smacks (community, установлен) | ✅ | + +## Что проверять клиентами + +- **Gajim/Dino/Conversations** — полная поддержка MUC, MAM, Carbons, OMEMO, Bookmarks (через PEP). +- **Converse.js (веб)** — MUC, MAM, Carbons, OMEMO (частично), HTTP Upload. +- Для OMEMO-шифрования: включить в клиенте, ключи хранятся в PEP (сервер уже готов). + +## Примечания + +- Модули OMEMO/Bookmarks как серверные отсутствуют в экосистеме Prosody 0.11 — + это норма: OMEMO и Bookmarks реализованы клиентски через PEP/pubsub. + Если нужен именно серверный mod_bookmarks (дефолтные закладки) — ставится + `mod_default_bookmarks` (community, hg.prosody.im). +- Источник community-модулей (работает из РФ): https://hg.prosody.im/prosody-modules/ + (GitHub raw заблокирован, 404). diff --git a/config/prosody.cfg.lua b/config/prosody.cfg.lua index 40a2155..19a9917 100644 --- a/config/prosody.cfg.lua +++ b/config/prosody.cfg.lua @@ -26,7 +26,9 @@ modules_enabled = { "dialback"; -- s2s dialback "disco"; -- Service discovery "carbons"; -- Копии сообщений на все устройства + "privilege"; -- XEP-0356 Privileged Entity (roster sync для мостов Slidge) "pep"; -- Personal eventing (OMEMO, avatars) + "pubsub"; -- Publish-Subscribe (XEP-0060): ленты, узлы; основа PEP/bookmarks "private"; -- Приватные хранилища XML "blocklist"; -- Чёрный список "vcard4"; -- vCard 4 @@ -76,6 +78,13 @@ http_interfaces = { "0.0.0.0" } https_ports = { 5281 } https_interfaces = { "0.0.0.0" } +-- External components (XEP-0114) для мостов (Slidge/Telegram, затем WhatsApp) +component_ports = { 5347 } +component_interfaces = { "0.0.0.0" } +component_secrets = { + ["telegram.nixg.ru"] = "83276db6e870f540163f271d9bd7d3a390530b9ad7b45f58", +} + -- Глобальный SSL для HTTPS-порта (LE-серт nixg.ru, SAN: nixg.ru + xmpp.nixg.ru) ssl = { key = "/etc/prosody/certs/nixg.ru.key"; @@ -99,6 +108,18 @@ VirtualHost "nixg.ru" certificate = "/etc/prosody/certs/nixg.ru.crt"; -- Для федерации нужен полный chain + private key } + -- Привилегии для мостов Slidge (XEP-0356): синхронизация ростера + legacy carbons + privileged_entities = { + ["telegram.nixg.ru"] = { + roster = "both"; + message = "outgoing"; + iq = { + ["http://jabber.org/protocol/pubsub"] = "both"; + ["http://jabber.org/protocol/pubsub#owner"] = "set"; + ["urn:xmpp:http:upload:0"] = "get"; + }; + }; + } -- MUC-компонент (групповые чаты) conference.nixg.ru Component "conference.nixg.ru" "muc" @@ -118,7 +139,8 @@ Component "upload.nixg.ru" "http_upload" -- Внешний URL (без :5281) — Caddy на vps02 проксирует upload.nixg.ru → 10.8.0.2:5281 http_external_url = "https://upload.nixg.ru" --- Мосты (mautrix) добавим на Этапе 3 — компоненты регистрируются через --- Prosody mod_component / отдельную конфигурацию. Пока не подключаем. --- Component "telegram.chat.nixg.ru" "xmpp_component" --- Component "whatsapp.chat.nixg.ru" "xmpp_component" \ No newline at end of file +-- Мосты (Slidge) — external component (XEP-0114) для Telegram. +-- Slidge подключается к component_ports (5347) с секретом из component_secrets. +Component "telegram.nixg.ru" + component_secret = "83276db6e870f540163f271d9bd7d3a390530b9ad7b45f58" + modules_enabled = { "disco", "privilege" } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d53ffcf..d362014 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,5 +35,24 @@ services: - ./webchat:/usr/share/nginx/html:ro ports: - "8081:8081" # веб-клиент (Caddy на vps02 проксирует на 10.8.0.2:8081) + depends_on: + - prosody + + # Мост XMPP -> Telegram (Slidge/slidgram), external component (XEP-0114) + # Трафик Telegram идёт через SOCKS5-прокси на хосте (172.27.0.1:1080 = SSH-туннель VPS01) + slidgram: + image: slidgram-proxy:latest + container_name: icq-slidgram + restart: unless-stopped + volumes: + - ./slidgram/data:/var/lib/slidge + environment: + - SLIDGE_JID=telegram.nixg.ru + - SLIDGE_SECRET=83276db6e870f540163f271d9bd7d3a390530b9ad7b45f58 + - SLIDGE_SERVER=icq-prosody + - SLIDGE_PORT=5347 + - SLIDGE_HOME_DIR=/var/lib/slidge + - SLIDGE_ADMINS=admin@nixg.ru + - SLIDGRAM_PROXY=socks5://172.27.0.1:1080 depends_on: - prosody \ No newline at end of file diff --git a/docs/slidgram/README.md b/docs/slidgram/README.md new file mode 100644 index 0000000..3ab5e71 --- /dev/null +++ b/docs/slidgram/README.md @@ -0,0 +1,27 @@ +# Документация slidgram (Slidge → Telegram) + +Сохранено из https://slidge.im/docs/slidgram/main/ (2026-08-29) — офлайн-копия +для проекта ICQ XMPP (/opt/icq). + +## Что это + +Документация **slidgram** — XMPP-шлюза (моста) к Telegram на базе Slidge. +Полезно, чтобы не ходить в интернет при настройке: все страницы, примеры +конфигов Prosody и описание опций — здесь. + +## Открытие + +- `main/index.html` — главная (открой в браузере) +- `main/admin/quickstart.html` — быстрый старт +- `main/admin/config.html` — все настройки (slidgram-specific + generic slidge) +- `main/admin/examples/index.html` — примеры конфигов XMPP-серверов: + **#prosody-upload**, **#prosody-no-upload** (Prosody), #ejabberd-upload и др. +- `main/admin/install.html` — установка +- `main/user/registration.html` — как зарегистрировать TG-аккаунт в мосте + +## Наш проект (bigbox /opt/icq) + +- Мост: сервис `slidgram` в docker-compose, образ `slidgram-proxy:latest` (с SOCKS5-патчем) +- Компонент: `telegram.nixg.ru` (порт 5347, XEP-0114) +- Трафик Telegram: через SOCKS5 `172.27.0.1:1080` (SSH-туннель VPS01) +- Секрет компонента: /opt/icq/config/prosody.cfg.lua → Component "telegram.nixg.ru" \ No newline at end of file diff --git a/docs/slidgram/main/_static/pygments.css b/docs/slidgram/main/_static/pygments.css new file mode 100644 index 0000000..9d1083b --- /dev/null +++ b/docs/slidgram/main/_static/pygments.css @@ -0,0 +1,250 @@ +.highlight pre { line-height: 125%; } +.highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +.highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #fdf2e2 } +.highlight { background: #f2f2f2; color: #1E1E1E } +.highlight .c { color: #515151 } /* Comment */ +.highlight .err { color: #D71835 } /* Error */ +.highlight .k { color: #8045E5 } /* Keyword */ +.highlight .l { color: #7F4707 } /* Literal */ +.highlight .n { color: #1E1E1E } /* Name */ +.highlight .o { color: #163 } /* Operator */ +.highlight .p { color: #1E1E1E } /* Punctuation */ +.highlight .ch { color: #515151 } /* Comment.Hashbang */ +.highlight .cm { color: #515151 } /* Comment.Multiline */ +.highlight .cp { color: #515151 } /* Comment.Preproc */ +.highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +.highlight .c1 { color: #515151 } /* Comment.Single */ +.highlight .cs { color: #515151 } /* Comment.Special */ +.highlight .gd { color: #00749C } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gh { color: #00749C } /* Generic.Heading */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #00749C } /* Generic.Subheading */ +.highlight .kc { color: #8045E5 } /* Keyword.Constant */ +.highlight .kd { color: #8045E5 } /* Keyword.Declaration */ +.highlight .kn { color: #8045E5 } /* Keyword.Namespace */ +.highlight .kp { color: #8045E5 } /* Keyword.Pseudo */ +.highlight .kr { color: #8045E5 } /* Keyword.Reserved */ +.highlight .kt { color: #7F4707 } /* Keyword.Type */ +.highlight .ld { color: #7F4707 } /* Literal.Date */ +.highlight .m { color: #7F4707 } /* Literal.Number */ +.highlight .s { color: #163 } /* Literal.String */ +.highlight .na { color: #7F4707 } /* Name.Attribute */ +.highlight .nb { color: #7F4707 } /* Name.Builtin */ +.highlight .nc { color: #00749C } /* Name.Class */ +.highlight .no { color: #00749C } /* Name.Constant */ +.highlight .nd { color: #7F4707 } /* Name.Decorator */ +.highlight .ni { color: #163 } /* Name.Entity */ +.highlight .ne { color: #8045E5 } /* Name.Exception */ +.highlight .nf { color: #00749C } /* Name.Function */ +.highlight .nl { color: #7F4707 } /* Name.Label */ +.highlight .nn { color: #1E1E1E } /* Name.Namespace */ +.highlight .nx { color: #1E1E1E } /* Name.Other */ +.highlight .py { color: #00749C } /* Name.Property */ +.highlight .nt { color: #00749C } /* Name.Tag */ +.highlight .nv { color: #D71835 } /* Name.Variable */ +.highlight .ow { color: #8045E5 } /* Operator.Word */ +.highlight .pm { color: #1E1E1E } /* Punctuation.Marker */ +.highlight .w { color: #1E1E1E } /* Text.Whitespace */ +.highlight .mb { color: #7F4707 } /* Literal.Number.Bin */ +.highlight .mf { color: #7F4707 } /* Literal.Number.Float */ +.highlight .mh { color: #7F4707 } /* Literal.Number.Hex */ +.highlight .mi { color: #7F4707 } /* Literal.Number.Integer */ +.highlight .mo { color: #7F4707 } /* Literal.Number.Oct */ +.highlight .sa { color: #163 } /* Literal.String.Affix */ +.highlight .sb { color: #163 } /* Literal.String.Backtick */ +.highlight .sc { color: #163 } /* Literal.String.Char */ +.highlight .dl { color: #163 } /* Literal.String.Delimiter */ +.highlight .sd { color: #163 } /* Literal.String.Doc */ +.highlight .s2 { color: #163 } /* Literal.String.Double */ +.highlight .se { color: #163 } /* Literal.String.Escape */ +.highlight .sh { color: #163 } /* Literal.String.Heredoc */ +.highlight .si { color: #163 } /* Literal.String.Interpol */ +.highlight .sx { color: #163 } /* Literal.String.Other */ +.highlight .sr { color: #D71835 } /* Literal.String.Regex */ +.highlight .s1 { color: #163 } /* Literal.String.Single */ +.highlight .ss { color: #00749C } /* Literal.String.Symbol */ +.highlight .bp { color: #7F4707 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00749C } /* Name.Function.Magic */ +.highlight .vc { color: #D71835 } /* Name.Variable.Class */ +.highlight .vg { color: #D71835 } /* Name.Variable.Global */ +.highlight .vi { color: #D71835 } /* Name.Variable.Instance */ +.highlight .vm { color: #7F4707 } /* Name.Variable.Magic */ +.highlight .il { color: #7F4707 } /* Literal.Number.Integer.Long */ +@media not print { +body[data-theme="dark"] .highlight pre { line-height: 125%; } +body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body[data-theme="dark"] .highlight .hll { background-color: #404040 } +body[data-theme="dark"] .highlight { background: #202020; color: #D0D0D0 } +body[data-theme="dark"] .highlight .c { color: #ABABAB; font-style: italic } /* Comment */ +body[data-theme="dark"] .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */ +body[data-theme="dark"] .highlight .esc { color: #D0D0D0 } /* Escape */ +body[data-theme="dark"] .highlight .g { color: #D0D0D0 } /* Generic */ +body[data-theme="dark"] .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */ +body[data-theme="dark"] .highlight .l { color: #D0D0D0 } /* Literal */ +body[data-theme="dark"] .highlight .n { color: #D0D0D0 } /* Name */ +body[data-theme="dark"] .highlight .o { color: #D0D0D0 } /* Operator */ +body[data-theme="dark"] .highlight .x { color: #D0D0D0 } /* Other */ +body[data-theme="dark"] .highlight .p { color: #D0D0D0 } /* Punctuation */ +body[data-theme="dark"] .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */ +body[data-theme="dark"] .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */ +body[data-theme="dark"] .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */ +body[data-theme="dark"] .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */ +body[data-theme="dark"] .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */ +body[data-theme="dark"] .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body[data-theme="dark"] .highlight .gd { color: #FF3A3A } /* Generic.Deleted */ +body[data-theme="dark"] .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */ +body[data-theme="dark"] .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body[data-theme="dark"] .highlight .gr { color: #FF3A3A } /* Generic.Error */ +body[data-theme="dark"] .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */ +body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */ +body[data-theme="dark"] .highlight .go { color: #CCC } /* Generic.Output */ +body[data-theme="dark"] .highlight .gp { color: #AAA } /* Generic.Prompt */ +body[data-theme="dark"] .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */ +body[data-theme="dark"] .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */ +body[data-theme="dark"] .highlight .gt { color: #FF3A3A } /* Generic.Traceback */ +body[data-theme="dark"] .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */ +body[data-theme="dark"] .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */ +body[data-theme="dark"] .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */ +body[data-theme="dark"] .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */ +body[data-theme="dark"] .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */ +body[data-theme="dark"] .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */ +body[data-theme="dark"] .highlight .ld { color: #D0D0D0 } /* Literal.Date */ +body[data-theme="dark"] .highlight .m { color: #51B2FD } /* Literal.Number */ +body[data-theme="dark"] .highlight .s { color: #ED9D13 } /* Literal.String */ +body[data-theme="dark"] .highlight .na { color: #BBB } /* Name.Attribute */ +body[data-theme="dark"] .highlight .nb { color: #2FBCCD } /* Name.Builtin */ +body[data-theme="dark"] .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */ +body[data-theme="dark"] .highlight .no { color: #40FFFF } /* Name.Constant */ +body[data-theme="dark"] .highlight .nd { color: #FFA500 } /* Name.Decorator */ +body[data-theme="dark"] .highlight .ni { color: #D0D0D0 } /* Name.Entity */ +body[data-theme="dark"] .highlight .ne { color: #BBB } /* Name.Exception */ +body[data-theme="dark"] .highlight .nf { color: #71ADFF } /* Name.Function */ +body[data-theme="dark"] .highlight .nl { color: #D0D0D0 } /* Name.Label */ +body[data-theme="dark"] .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */ +body[data-theme="dark"] .highlight .nx { color: #D0D0D0 } /* Name.Other */ +body[data-theme="dark"] .highlight .py { color: #D0D0D0 } /* Name.Property */ +body[data-theme="dark"] .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */ +body[data-theme="dark"] .highlight .nv { color: #40FFFF } /* Name.Variable */ +body[data-theme="dark"] .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */ +body[data-theme="dark"] .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */ +body[data-theme="dark"] .highlight .w { color: #666 } /* Text.Whitespace */ +body[data-theme="dark"] .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */ +body[data-theme="dark"] .highlight .mf { color: #51B2FD } /* Literal.Number.Float */ +body[data-theme="dark"] .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */ +body[data-theme="dark"] .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */ +body[data-theme="dark"] .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */ +body[data-theme="dark"] .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */ +body[data-theme="dark"] .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */ +body[data-theme="dark"] .highlight .sc { color: #ED9D13 } /* Literal.String.Char */ +body[data-theme="dark"] .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */ +body[data-theme="dark"] .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */ +body[data-theme="dark"] .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */ +body[data-theme="dark"] .highlight .se { color: #ED9D13 } /* Literal.String.Escape */ +body[data-theme="dark"] .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */ +body[data-theme="dark"] .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */ +body[data-theme="dark"] .highlight .sx { color: #FFA500 } /* Literal.String.Other */ +body[data-theme="dark"] .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */ +body[data-theme="dark"] .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */ +body[data-theme="dark"] .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */ +body[data-theme="dark"] .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */ +body[data-theme="dark"] .highlight .fm { color: #71ADFF } /* Name.Function.Magic */ +body[data-theme="dark"] .highlight .vc { color: #40FFFF } /* Name.Variable.Class */ +body[data-theme="dark"] .highlight .vg { color: #40FFFF } /* Name.Variable.Global */ +body[data-theme="dark"] .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */ +body[data-theme="dark"] .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */ +body[data-theme="dark"] .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */ +@media (prefers-color-scheme: dark) { +body:not([data-theme="light"]) .highlight pre { line-height: 125%; } +body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +body:not([data-theme="light"]) .highlight .hll { background-color: #404040 } +body:not([data-theme="light"]) .highlight { background: #202020; color: #D0D0D0 } +body:not([data-theme="light"]) .highlight .c { color: #ABABAB; font-style: italic } /* Comment */ +body:not([data-theme="light"]) .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */ +body:not([data-theme="light"]) .highlight .esc { color: #D0D0D0 } /* Escape */ +body:not([data-theme="light"]) .highlight .g { color: #D0D0D0 } /* Generic */ +body:not([data-theme="light"]) .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */ +body:not([data-theme="light"]) .highlight .l { color: #D0D0D0 } /* Literal */ +body:not([data-theme="light"]) .highlight .n { color: #D0D0D0 } /* Name */ +body:not([data-theme="light"]) .highlight .o { color: #D0D0D0 } /* Operator */ +body:not([data-theme="light"]) .highlight .x { color: #D0D0D0 } /* Other */ +body:not([data-theme="light"]) .highlight .p { color: #D0D0D0 } /* Punctuation */ +body:not([data-theme="light"]) .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */ +body:not([data-theme="light"]) .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */ +body:not([data-theme="light"]) .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */ +body:not([data-theme="light"]) .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */ +body:not([data-theme="light"]) .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */ +body:not([data-theme="light"]) .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */ +body:not([data-theme="light"]) .highlight .gd { color: #FF3A3A } /* Generic.Deleted */ +body:not([data-theme="light"]) .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */ +body:not([data-theme="light"]) .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +body:not([data-theme="light"]) .highlight .gr { color: #FF3A3A } /* Generic.Error */ +body:not([data-theme="light"]) .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */ +body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */ +body:not([data-theme="light"]) .highlight .go { color: #CCC } /* Generic.Output */ +body:not([data-theme="light"]) .highlight .gp { color: #AAA } /* Generic.Prompt */ +body:not([data-theme="light"]) .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */ +body:not([data-theme="light"]) .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */ +body:not([data-theme="light"]) .highlight .gt { color: #FF3A3A } /* Generic.Traceback */ +body:not([data-theme="light"]) .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */ +body:not([data-theme="light"]) .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */ +body:not([data-theme="light"]) .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */ +body:not([data-theme="light"]) .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */ +body:not([data-theme="light"]) .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */ +body:not([data-theme="light"]) .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */ +body:not([data-theme="light"]) .highlight .ld { color: #D0D0D0 } /* Literal.Date */ +body:not([data-theme="light"]) .highlight .m { color: #51B2FD } /* Literal.Number */ +body:not([data-theme="light"]) .highlight .s { color: #ED9D13 } /* Literal.String */ +body:not([data-theme="light"]) .highlight .na { color: #BBB } /* Name.Attribute */ +body:not([data-theme="light"]) .highlight .nb { color: #2FBCCD } /* Name.Builtin */ +body:not([data-theme="light"]) .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */ +body:not([data-theme="light"]) .highlight .no { color: #40FFFF } /* Name.Constant */ +body:not([data-theme="light"]) .highlight .nd { color: #FFA500 } /* Name.Decorator */ +body:not([data-theme="light"]) .highlight .ni { color: #D0D0D0 } /* Name.Entity */ +body:not([data-theme="light"]) .highlight .ne { color: #BBB } /* Name.Exception */ +body:not([data-theme="light"]) .highlight .nf { color: #71ADFF } /* Name.Function */ +body:not([data-theme="light"]) .highlight .nl { color: #D0D0D0 } /* Name.Label */ +body:not([data-theme="light"]) .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */ +body:not([data-theme="light"]) .highlight .nx { color: #D0D0D0 } /* Name.Other */ +body:not([data-theme="light"]) .highlight .py { color: #D0D0D0 } /* Name.Property */ +body:not([data-theme="light"]) .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */ +body:not([data-theme="light"]) .highlight .nv { color: #40FFFF } /* Name.Variable */ +body:not([data-theme="light"]) .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */ +body:not([data-theme="light"]) .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */ +body:not([data-theme="light"]) .highlight .w { color: #666 } /* Text.Whitespace */ +body:not([data-theme="light"]) .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */ +body:not([data-theme="light"]) .highlight .mf { color: #51B2FD } /* Literal.Number.Float */ +body:not([data-theme="light"]) .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */ +body:not([data-theme="light"]) .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */ +body:not([data-theme="light"]) .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */ +body:not([data-theme="light"]) .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */ +body:not([data-theme="light"]) .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */ +body:not([data-theme="light"]) .highlight .sc { color: #ED9D13 } /* Literal.String.Char */ +body:not([data-theme="light"]) .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */ +body:not([data-theme="light"]) .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */ +body:not([data-theme="light"]) .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */ +body:not([data-theme="light"]) .highlight .se { color: #ED9D13 } /* Literal.String.Escape */ +body:not([data-theme="light"]) .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */ +body:not([data-theme="light"]) .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */ +body:not([data-theme="light"]) .highlight .sx { color: #FFA500 } /* Literal.String.Other */ +body:not([data-theme="light"]) .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */ +body:not([data-theme="light"]) .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */ +body:not([data-theme="light"]) .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */ +body:not([data-theme="light"]) .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */ +body:not([data-theme="light"]) .highlight .fm { color: #71ADFF } /* Name.Function.Magic */ +body:not([data-theme="light"]) .highlight .vc { color: #40FFFF } /* Name.Variable.Class */ +body:not([data-theme="light"]) .highlight .vg { color: #40FFFF } /* Name.Variable.Global */ +body:not([data-theme="light"]) .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */ +body:not([data-theme="light"]) .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */ +body:not([data-theme="light"]) .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */ +} +} \ No newline at end of file diff --git a/docs/slidgram/main/_static/styles/furo-extensions.css b/docs/slidgram/main/_static/styles/furo-extensions.css new file mode 100644 index 0000000..2d74267 --- /dev/null +++ b/docs/slidgram/main/_static/styles/furo-extensions.css @@ -0,0 +1,2 @@ +#furo-sidebar-ad-placement{padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)}#furo-sidebar-ad-placement .ethical-sidebar{background:var(--color-background-secondary);border:none;box-shadow:none}#furo-sidebar-ad-placement .ethical-sidebar:hover{background:var(--color-background-hover)}#furo-sidebar-ad-placement .ethical-sidebar a{color:var(--color-foreground-primary)}#furo-sidebar-ad-placement .ethical-callout a{color:var(--color-foreground-secondary)!important}#furo-readthedocs-versions{background:transparent;display:block;position:static;width:100%}#furo-readthedocs-versions .rst-versions{background:#1a1c1e}#furo-readthedocs-versions .rst-current-version{background:var(--color-sidebar-item-background);cursor:unset}#furo-readthedocs-versions .rst-current-version:hover{background:var(--color-sidebar-item-background)}#furo-readthedocs-versions .rst-current-version .fa-book{color:var(--color-foreground-primary)}#furo-readthedocs-versions>.rst-other-versions{padding:0}#furo-readthedocs-versions>.rst-other-versions small{opacity:1}#furo-readthedocs-versions .injected .rst-versions{position:unset}#furo-readthedocs-versions:focus-within,#furo-readthedocs-versions:hover{box-shadow:0 0 0 1px var(--color-sidebar-background-border)}#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:hover .rst-current-version{background:#1a1c1e;font-size:inherit;height:auto;line-height:inherit;padding:12px;text-align:right}#furo-readthedocs-versions:focus-within .rst-current-version .fa-book,#furo-readthedocs-versions:hover .rst-current-version .fa-book{color:#fff;float:left}#furo-readthedocs-versions:focus-within .fa-caret-down,#furo-readthedocs-versions:hover .fa-caret-down{display:none}#furo-readthedocs-versions:focus-within .injected,#furo-readthedocs-versions:focus-within .rst-current-version,#furo-readthedocs-versions:focus-within .rst-other-versions,#furo-readthedocs-versions:hover .injected,#furo-readthedocs-versions:hover .rst-current-version,#furo-readthedocs-versions:hover .rst-other-versions{display:block}#furo-readthedocs-versions:focus-within>.rst-current-version,#furo-readthedocs-versions:hover>.rst-current-version{display:none}.highlight:hover button.copybtn{color:var(--color-code-foreground)}.highlight button.copybtn{align-items:center;background-color:var(--color-code-background);border:none;color:var(--color-background-item);cursor:pointer;height:1.25em;right:.5rem;top:.625rem;transition:color .3s,opacity .3s;width:1.25em}.highlight button.copybtn:hover{background-color:var(--color-code-background);color:var(--color-brand-content)}.highlight button.copybtn:after{background-color:transparent;color:var(--color-code-foreground);display:none}.highlight button.copybtn.success{color:#22863a;transition:color 0s}.highlight button.copybtn.success:after{display:block}.highlight button.copybtn svg{padding:0}body{--sd-color-primary:var(--color-brand-primary);--sd-color-primary-highlight:var(--color-brand-content);--sd-color-primary-text:var(--color-background-primary);--sd-color-shadow:rgba(0,0,0,.05);--sd-color-card-border:var(--color-card-border);--sd-color-card-border-hover:var(--color-brand-content);--sd-color-card-background:var(--color-card-background);--sd-color-card-text:var(--color-foreground-primary);--sd-color-card-header:var(--color-card-marginals-background);--sd-color-card-footer:var(--color-card-marginals-background);--sd-color-tabs-label-active:var(--color-brand-content);--sd-color-tabs-label-hover:var(--color-foreground-muted);--sd-color-tabs-label-inactive:var(--color-foreground-muted);--sd-color-tabs-underline-active:var(--color-brand-content);--sd-color-tabs-underline-hover:var(--color-foreground-border);--sd-color-tabs-underline-inactive:var(--color-background-border);--sd-color-tabs-overline:var(--color-background-border);--sd-color-tabs-underline:var(--color-background-border)}.sd-tab-content{box-shadow:0 -2px var(--sd-color-tabs-overline),0 1px var(--sd-color-tabs-underline)}.sd-card{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)}.sd-shadow-sm{box-shadow:0 .1rem .25rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-md{box-shadow:0 .3rem .75rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-shadow-lg{box-shadow:0 .6rem 1.5rem var(--sd-color-shadow),0 0 .0625rem rgba(0,0,0,.1)!important}.sd-card-hover:hover{transform:none}.sd-cards-carousel{gap:.25rem;padding:.25rem}body{--tabs--label-text:var(--color-foreground-muted);--tabs--label-text--hover:var(--color-foreground-muted);--tabs--label-text--active:var(--color-brand-content);--tabs--label-text--active--hover:var(--color-brand-content);--tabs--label-background:transparent;--tabs--label-background--hover:transparent;--tabs--label-background--active:transparent;--tabs--label-background--active--hover:transparent;--tabs--padding-x:0.25em;--tabs--margin-x:1em;--tabs--border:var(--color-background-border);--tabs--label-border:transparent;--tabs--label-border--hover:var(--color-foreground-muted);--tabs--label-border--active:var(--color-brand-content);--tabs--label-border--active--hover:var(--color-brand-content)}[role=main] .container{max-width:none;padding-left:0;padding-right:0}.shadow.docutils{border:none;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)!important}.sphinx-bs .card{background-color:var(--color-background-secondary);color:var(--color-foreground)} +/*# sourceMappingURL=furo-extensions.css.map*/ \ No newline at end of file diff --git a/docs/slidgram/main/_static/styles/furo.css b/docs/slidgram/main/_static/styles/furo.css new file mode 100644 index 0000000..a5b614d --- /dev/null +++ b/docs/slidgram/main/_static/styles/furo.css @@ -0,0 +1,2 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}@media print{.content-icon-container,.headerlink,.mobile-header,.related-pages{display:none!important}.highlight{border:.1pt solid var(--color-foreground-border)}a,blockquote,dl,ol,p,pre,table,ul{page-break-inside:avoid}caption,figure,h1,h2,h3,h4,h5,h6,img{page-break-after:avoid;page-break-inside:avoid}dl,ol,ul{page-break-before:avoid}}.visually-hidden{height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;clip:rect(0,0,0,0)!important;background:var(--color-background-primary);border:0!important;color:var(--color-foreground-primary);white-space:nowrap!important}:-moz-focusring{outline:auto}body{--font-stack:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;--font-stack--monospace:"SFMono-Regular",Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace;--font-stack--headings:var(--font-stack);--font-size--normal:100%;--font-size--small:87.5%;--font-size--small--2:81.25%;--font-size--small--3:75%;--font-size--small--4:62.5%;--sidebar-caption-font-size:var(--font-size--small--2);--sidebar-item-font-size:var(--font-size--small);--sidebar-search-input-font-size:var(--font-size--small);--toc-font-size:var(--font-size--small--3);--toc-font-size--mobile:var(--font-size--normal);--toc-title-font-size:var(--font-size--small--4);--admonition-font-size:0.8125rem;--admonition-title-font-size:0.8125rem;--code-font-size:var(--font-size--small--2);--api-font-size:var(--font-size--small);--header-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*4);--header-padding:0.5rem;--sidebar-tree-space-above:1.5rem;--sidebar-caption-space-above:1rem;--sidebar-item-line-height:1rem;--sidebar-item-spacing-vertical:0.5rem;--sidebar-item-spacing-horizontal:1rem;--sidebar-item-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*2);--sidebar-expander-width:var(--sidebar-item-height);--sidebar-search-space-above:0.5rem;--sidebar-search-input-spacing-vertical:0.5rem;--sidebar-search-input-spacing-horizontal:0.5rem;--sidebar-search-input-height:1rem;--sidebar-search-icon-size:var(--sidebar-search-input-height);--toc-title-padding:0.25rem 0;--toc-spacing-vertical:1.5rem;--toc-spacing-horizontal:1.5rem;--toc-item-spacing-vertical:0.4rem;--toc-item-spacing-horizontal:1rem;--icon-search:url('data:image/svg+xml;charset=utf-8,');--icon-pencil:url('data:image/svg+xml;charset=utf-8,');--icon-abstract:url('data:image/svg+xml;charset=utf-8,');--icon-info:url('data:image/svg+xml;charset=utf-8,');--icon-flame:url('data:image/svg+xml;charset=utf-8,');--icon-question:url('data:image/svg+xml;charset=utf-8,');--icon-warning:url('data:image/svg+xml;charset=utf-8,');--icon-failure:url('data:image/svg+xml;charset=utf-8,');--icon-spark:url('data:image/svg+xml;charset=utf-8,');--color-admonition-title--caution:#ff9100;--color-admonition-title-background--caution:rgba(255,145,0,.2);--color-admonition-title--warning:#ff9100;--color-admonition-title-background--warning:rgba(255,145,0,.2);--color-admonition-title--danger:#ff5252;--color-admonition-title-background--danger:rgba(255,82,82,.2);--color-admonition-title--attention:#ff5252;--color-admonition-title-background--attention:rgba(255,82,82,.2);--color-admonition-title--error:#ff5252;--color-admonition-title-background--error:rgba(255,82,82,.2);--color-admonition-title--hint:#00c852;--color-admonition-title-background--hint:rgba(0,200,82,.2);--color-admonition-title--tip:#00c852;--color-admonition-title-background--tip:rgba(0,200,82,.2);--color-admonition-title--important:#00bfa5;--color-admonition-title-background--important:rgba(0,191,165,.2);--color-admonition-title--note:#00b0ff;--color-admonition-title-background--note:rgba(0,176,255,.2);--color-admonition-title--seealso:#448aff;--color-admonition-title-background--seealso:rgba(68,138,255,.2);--color-admonition-title--admonition-todo:grey;--color-admonition-title-background--admonition-todo:hsla(0,0%,50%,.2);--color-admonition-title:#651fff;--color-admonition-title-background:rgba(101,31,255,.2);--icon-admonition-default:var(--icon-abstract);--color-topic-title:#14b8a6;--color-topic-title-background:rgba(20,184,166,.2);--icon-topic-default:var(--icon-pencil);--color-problematic:#b30000;--color-foreground-primary:#000;--color-foreground-secondary:#5a5c63;--color-foreground-muted:#6b6f76;--color-foreground-border:#878787;--color-background-primary:#fff;--color-background-secondary:#f8f9fb;--color-background-hover:#efeff4;--color-background-hover--transparent:#efeff400;--color-background-border:#eeebee;--color-background-item:#ccc;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#0a4bff;--color-brand-content:#2757dd;--color-brand-visited:#872ee0;--color-api-background:var(--color-background-hover--transparent);--color-api-background-hover:var(--color-background-hover);--color-api-overall:var(--color-foreground-secondary);--color-api-name:var(--color-problematic);--color-api-pre-name:var(--color-problematic);--color-api-paren:var(--color-foreground-secondary);--color-api-keyword:var(--color-foreground-primary);--color-api-added:#21632c;--color-api-added-border:#38a84d;--color-api-changed:#046172;--color-api-changed-border:#06a1bc;--color-api-deprecated:#605706;--color-api-deprecated-border:#f0d90f;--color-api-removed:#b30000;--color-api-removed-border:#ff5c5c;--color-highlight-on-target:#ffc;--color-inline-code-background:var(--color-background-secondary);--color-highlighted-background:#def;--color-highlighted-text:var(--color-foreground-primary);--color-guilabel-background:#ddeeff80;--color-guilabel-border:#bedaf580;--color-guilabel-text:var(--color-foreground-primary);--color-admonition-background:transparent;--color-table-header-background:var(--color-background-secondary);--color-table-border:var(--color-background-border);--color-card-border:var(--color-background-secondary);--color-card-background:transparent;--color-card-marginals-background:var(--color-background-secondary);--color-header-background:var(--color-background-primary);--color-header-border:var(--color-background-border);--color-header-text:var(--color-foreground-primary);--color-sidebar-background:var(--color-background-secondary);--color-sidebar-background-border:var(--color-background-border);--color-sidebar-brand-text:var(--color-foreground-primary);--color-sidebar-caption-text:var(--color-foreground-muted);--color-sidebar-link-text:var(--color-foreground-secondary);--color-sidebar-link-text--top-level:var(--color-brand-primary);--color-sidebar-item-background:var(--color-sidebar-background);--color-sidebar-item-background--current:var( --color-sidebar-item-background );--color-sidebar-item-background--hover:linear-gradient(90deg,var(--color-background-hover--transparent) 0%,var(--color-background-hover) var(--sidebar-item-spacing-horizontal),var(--color-background-hover) 100%);--color-sidebar-item-expander-background:transparent;--color-sidebar-item-expander-background--hover:var( --color-background-hover );--color-sidebar-search-text:var(--color-foreground-primary);--color-sidebar-search-background:var(--color-background-secondary);--color-sidebar-search-background--focus:var(--color-background-primary);--color-sidebar-search-border:var(--color-background-border);--color-sidebar-search-icon:var(--color-foreground-muted);--color-toc-background:var(--color-background-primary);--color-toc-title-text:var(--color-foreground-muted);--color-toc-item-text:var(--color-foreground-secondary);--color-toc-item-text--hover:var(--color-foreground-primary);--color-toc-item-text--active:var(--color-brand-primary);--color-content-foreground:var(--color-foreground-primary);--color-content-background:transparent;--color-link:var(--color-brand-content);--color-link-underline:var(--color-background-border);--color-link--hover:var(--color-brand-content);--color-link-underline--hover:var(--color-foreground-border);--color-link--visited:var(--color-brand-visited);--color-link-underline--visited:var(--color-background-border);--color-link--visited--hover:var(--color-brand-visited);--color-link-underline--visited--hover:var(--color-foreground-border)}.only-light{display:block!important}html body .only-dark{display:none!important}@media not print{body[data-theme=dark]{--color-problematic:#ee5151;--color-foreground-primary:#cfd0d0;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body[data-theme=dark] .only-light{display:none!important}body[data-theme=dark] .only-dark{display:block!important}@media(prefers-color-scheme:dark){body:not([data-theme=light]){--color-problematic:#ee5151;--color-foreground-primary:#cfd0d0;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#3d94ff;--color-brand-content:#5ca5ff;--color-brand-visited:#b27aeb;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-api-added:#3db854;--color-api-added-border:#267334;--color-api-changed:#09b0ce;--color-api-changed-border:#056d80;--color-api-deprecated:#b1a10b;--color-api-deprecated-border:#6e6407;--color-api-removed:#ff7575;--color-api-removed-border:#b03b3b;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body:not([data-theme=light]) .only-light{display:none!important}body:not([data-theme=light]) .only-dark{display:block!important}}}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-light{display:block}@media(prefers-color-scheme:dark){body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-dark{display:block}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto-light{display:none}}body[data-theme=dark] .theme-toggle svg.theme-icon-when-dark,body[data-theme=light] .theme-toggle svg.theme-icon-when-light{display:block}body{font-family:var(--font-stack)}code,kbd,pre,samp{font-family:var(--font-stack--monospace)}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}article{line-height:1.5}h1,h2,h3,h4,h5,h6{border-radius:.5rem;font-family:var(--font-stack--headings);font-weight:700;line-height:1.25;margin:.5rem -.5rem;padding-left:.5rem;padding-right:.5rem}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p{margin-top:0}h1{font-size:2.5em;margin-bottom:1rem}h1,h2{margin-top:1.75rem}h2{font-size:2em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1.125em}h6{font-size:1em}small{font-size:80%;opacity:75%}p{margin-bottom:.75rem;margin-top:.5rem}hr.docutils{background-color:var(--color-background-border);border:0;height:1px;margin:2rem 0;padding:0}.centered{text-align:center}a{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}a:visited{color:var(--color-link--visited);text-decoration-color:var(--color-link-underline--visited)}a:visited:hover{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--hover)}a:hover{color:var(--color-link--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link{color:inherit}a.muted-link:hover{color:var(--color-link--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link:hover:visited{color:var(--color-link--visited--hover);text-decoration-color:var(--color-link-underline--visited--hover)}html{overflow-x:hidden;overflow-y:scroll;scroll-behavior:smooth}.sidebar-scroll,.toc-scroll,article[role=main] *{scrollbar-color:var(--color-foreground-border) transparent;scrollbar-width:thin}body,html{height:100%}.skip-to-content,body,html{background:var(--color-background-primary);color:var(--color-foreground-primary)}.skip-to-content{border-radius:1rem;left:.25rem;padding:1rem;position:fixed;top:.25rem;transform:translateY(-200%);transition:transform .3s ease-in-out;z-index:40}.skip-to-content:focus-within{transform:translateY(0)}article{background:var(--color-content-background);color:var(--color-content-foreground);overflow-wrap:break-word}.page{display:flex;min-height:100%}.mobile-header{background-color:var(--color-header-background);border-bottom:1px solid var(--color-header-border);color:var(--color-header-text);display:none;height:var(--header-height);width:100%;z-index:10}.mobile-header.scrolled{border-bottom:none;box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2)}.mobile-header .header-center a{color:var(--color-header-text);text-decoration:none}.main{display:flex;flex:1}.sidebar-drawer{background:var(--color-sidebar-background);border-right:1px solid var(--color-sidebar-background-border);box-sizing:border-box;display:flex;justify-content:flex-end;min-width:15em;width:calc(50% - 26em)}.sidebar-container,.toc-drawer{box-sizing:border-box;width:15em}.toc-drawer{background:var(--color-toc-background);padding-right:1rem}.sidebar-sticky,.toc-sticky{display:flex;flex-direction:column;height:min(100%,100vh);height:100vh;position:sticky;top:0}.sidebar-scroll,.toc-scroll{flex-grow:1;flex-shrink:1;overflow:auto;scroll-behavior:smooth}.content{display:flex;flex-direction:column;justify-content:space-between;padding:0 3em;width:46em}.icon{display:inline-block;height:1rem;width:1rem}.icon svg{height:100%;width:100%}.announcement{align-items:center;background-color:var(--color-announcement-background);color:var(--color-announcement-text);display:flex;height:var(--header-height);overflow-x:auto}.announcement+.page{min-height:calc(100% - var(--header-height))}.announcement-content{box-sizing:border-box;min-width:100%;padding:.5rem;text-align:center;white-space:nowrap}.announcement-content a{color:var(--color-announcement-text);text-decoration-color:var(--color-announcement-text)}.announcement-content a:hover{color:var(--color-announcement-text);text-decoration-color:var(--color-link--hover)}.no-js .theme-toggle-container{display:none}.theme-toggle-container{display:flex}.theme-toggle{background:transparent;border:none;cursor:pointer;display:flex;padding:0}.theme-toggle svg{color:var(--color-foreground-primary);display:none;height:1.25rem;width:1.25rem}.theme-toggle-header{align-items:center;display:flex;justify-content:center}.nav-overlay-icon,.toc-overlay-icon{cursor:pointer;display:none}.nav-overlay-icon .icon,.toc-overlay-icon .icon{color:var(--color-foreground-secondary);height:1.5rem;width:1.5rem}.nav-overlay-icon,.toc-header-icon{align-items:center;justify-content:center}.toc-content-icon{height:1.5rem;width:1.5rem}.content-icon-container{display:flex;float:right;gap:.5rem;margin-bottom:1rem;margin-left:1rem;margin-top:1.5rem}.content-icon-container .edit-this-page svg,.content-icon-container .view-this-page svg{color:inherit;height:1.25rem;width:1.25rem}.sidebar-toggle{display:none;position:absolute}.sidebar-toggle[name=__toc]{left:20px}.sidebar-toggle:checked{left:40px}.overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0s,height 0s,opacity .25s ease-out;width:0}.sidebar-overlay{z-index:20}.toc-overlay{z-index:40}.sidebar-drawer{transition:left .25s ease-in-out;z-index:30}.toc-drawer{transition:right .25s ease-in-out;z-index:50}#__navigation:checked~.sidebar-overlay{height:100%;opacity:1;width:100%}#__navigation:checked~.page .sidebar-drawer{left:0;top:0}#__toc:checked~.toc-overlay{height:100%;opacity:1;width:100%}#__toc:checked~.page .toc-drawer{right:0;top:0}.back-to-top{background:var(--color-background-primary);border-radius:1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 1px 0 hsla(220,9%,46%,.502);display:none;font-size:.8125rem;left:0;margin-left:50%;padding:.5rem .75rem .5rem .5rem;position:fixed;text-decoration:none;top:1rem;transform:translateX(-50%);z-index:10}.back-to-top svg{height:1rem;width:1rem;fill:currentColor;display:inline-block}.back-to-top span{margin-left:.25rem}.show-back-to-top .back-to-top{align-items:center;display:flex}@media(min-width:97em){html{font-size:110%}}@media(max-width:82em){.toc-content-icon{display:flex}.toc-drawer{border-left:1px solid var(--color-background-muted);height:100vh;position:fixed;right:-15em;top:0}.toc-tree{border-left:none;font-size:var(--toc-font-size--mobile)}.sidebar-drawer{width:calc(50% - 18.5em)}}@media(max-width:67em){.content{margin-left:auto;margin-right:auto;padding:0 1em}}@media(max-width:63em){.nav-overlay-icon{display:flex}.sidebar-drawer{height:100vh;left:-15em;position:fixed;top:0;width:15em}.theme-toggle-header,.toc-header-icon{display:flex}.theme-toggle-content,.toc-content-icon{display:none}.mobile-header{align-items:center;display:flex;justify-content:space-between;position:sticky;top:0}.mobile-header .header-left,.mobile-header .header-right{display:flex;height:var(--header-height);padding:0 var(--header-padding)}.mobile-header .header-left label,.mobile-header .header-right label{height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.nav-overlay-icon .icon,.theme-toggle svg{height:1.5rem;width:1.5rem}:target{scroll-margin-top:calc(var(--header-height) + 2.5rem)}.back-to-top{top:calc(var(--header-height) + .5rem)}.page{flex-direction:column;justify-content:center}}@media(max-width:48em){.content{overflow-x:auto;width:100%}}@media(max-width:46em){article[role=main] aside.sidebar{float:none;margin:1rem 0;width:100%}}.admonition,.topic{background:var(--color-admonition-background);border-radius:.2rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1);font-size:var(--admonition-font-size);margin:1rem auto;overflow:hidden;padding:0 .5rem .5rem;page-break-inside:avoid}.admonition>:nth-child(2),.topic>:nth-child(2){margin-top:0}.admonition>:last-child,.topic>:last-child{margin-bottom:0}.admonition p.admonition-title,p.topic-title{font-size:var(--admonition-title-font-size);font-weight:500;line-height:1.3;margin:0 -.5rem .5rem;padding:.4rem .5rem .4rem 2rem;position:relative}.admonition p.admonition-title:before,p.topic-title:before{content:"";height:1rem;left:.5rem;position:absolute;width:1rem}p.admonition-title{background-color:var(--color-admonition-title-background)}p.admonition-title:before{background-color:var(--color-admonition-title);-webkit-mask-image:var(--icon-admonition-default);mask-image:var(--icon-admonition-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}p.topic-title{background-color:var(--color-topic-title-background)}p.topic-title:before{background-color:var(--color-topic-title);-webkit-mask-image:var(--icon-topic-default);mask-image:var(--icon-topic-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.admonition{border-left:.2rem solid var(--color-admonition-title)}.admonition.caution{border-left-color:var(--color-admonition-title--caution)}.admonition.caution>.admonition-title{background-color:var(--color-admonition-title-background--caution)}.admonition.caution>.admonition-title:before{background-color:var(--color-admonition-title--caution);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.warning{border-left-color:var(--color-admonition-title--warning)}.admonition.warning>.admonition-title{background-color:var(--color-admonition-title-background--warning)}.admonition.warning>.admonition-title:before{background-color:var(--color-admonition-title--warning);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.danger{border-left-color:var(--color-admonition-title--danger)}.admonition.danger>.admonition-title{background-color:var(--color-admonition-title-background--danger)}.admonition.danger>.admonition-title:before{background-color:var(--color-admonition-title--danger);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.attention{border-left-color:var(--color-admonition-title--attention)}.admonition.attention>.admonition-title{background-color:var(--color-admonition-title-background--attention)}.admonition.attention>.admonition-title:before{background-color:var(--color-admonition-title--attention);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.error{border-left-color:var(--color-admonition-title--error)}.admonition.error>.admonition-title{background-color:var(--color-admonition-title-background--error)}.admonition.error>.admonition-title:before{background-color:var(--color-admonition-title--error);-webkit-mask-image:var(--icon-failure);mask-image:var(--icon-failure)}.admonition.hint{border-left-color:var(--color-admonition-title--hint)}.admonition.hint>.admonition-title{background-color:var(--color-admonition-title-background--hint)}.admonition.hint>.admonition-title:before{background-color:var(--color-admonition-title--hint);-webkit-mask-image:var(--icon-question);mask-image:var(--icon-question)}.admonition.tip{border-left-color:var(--color-admonition-title--tip)}.admonition.tip>.admonition-title{background-color:var(--color-admonition-title-background--tip)}.admonition.tip>.admonition-title:before{background-color:var(--color-admonition-title--tip);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.important{border-left-color:var(--color-admonition-title--important)}.admonition.important>.admonition-title{background-color:var(--color-admonition-title-background--important)}.admonition.important>.admonition-title:before{background-color:var(--color-admonition-title--important);-webkit-mask-image:var(--icon-flame);mask-image:var(--icon-flame)}.admonition.note{border-left-color:var(--color-admonition-title--note)}.admonition.note>.admonition-title{background-color:var(--color-admonition-title-background--note)}.admonition.note>.admonition-title:before{background-color:var(--color-admonition-title--note);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition.seealso{border-left-color:var(--color-admonition-title--seealso)}.admonition.seealso>.admonition-title{background-color:var(--color-admonition-title-background--seealso)}.admonition.seealso>.admonition-title:before{background-color:var(--color-admonition-title--seealso);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.admonition-todo{border-left-color:var(--color-admonition-title--admonition-todo)}.admonition.admonition-todo>.admonition-title{background-color:var(--color-admonition-title-background--admonition-todo)}.admonition.admonition-todo>.admonition-title:before{background-color:var(--color-admonition-title--admonition-todo);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition-todo>.admonition-title{text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd{margin-left:2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:first-child{margin-top:.125rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list,dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:last-child{margin-bottom:.75rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list>dt{font-size:var(--font-size--small);text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd:empty{margin-bottom:.5rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul{margin-left:-1.2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p:nth-child(2){margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p+p:last-child:empty{margin-bottom:0;margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{color:var(--color-api-overall)}.sig:not(.sig-inline){background:var(--color-api-background);border-radius:.25rem;font-family:var(--font-stack--monospace);font-size:var(--api-font-size);font-weight:700;margin-left:-.25rem;margin-right:-.25rem;padding:.25rem .5rem .25rem 3em;text-indent:-2.5em;transition:background .1s ease-out}.sig:not(.sig-inline):hover{background:var(--color-api-background-hover)}.sig:not(.sig-inline) a.reference .viewcode-link{font-weight:400;width:4.25rem}em.property,span.property{font-style:normal}em.property:first-child,span.property:first-child{color:var(--color-api-keyword)}.sig-name{color:var(--color-api-name)}.sig-prename{color:var(--color-api-pre-name);font-weight:400}.sig-paren{color:var(--color-api-paren)}.sig-param{font-style:normal}div.deprecated,div.versionadded,div.versionchanged,div.versionremoved{border-left:.1875rem solid;border-radius:.125rem;padding-left:.75rem}div.deprecated p,div.versionadded p,div.versionchanged p,div.versionremoved p{margin-bottom:.125rem;margin-top:.125rem}div.versionadded{border-color:var(--color-api-added-border)}div.versionadded .versionmodified{color:var(--color-api-added)}div.versionchanged{border-color:var(--color-api-changed-border)}div.versionchanged .versionmodified{color:var(--color-api-changed)}div.deprecated{border-color:var(--color-api-deprecated-border)}div.deprecated .versionmodified{color:var(--color-api-deprecated)}div.versionremoved{border-color:var(--color-api-removed-border)}div.versionremoved .versionmodified{color:var(--color-api-removed)}.viewcode-back,.viewcode-link{float:right;text-align:right}.line-block{margin-bottom:.75rem;margin-top:.5rem}.line-block .line-block{margin-bottom:0;margin-top:0;padding-left:1rem}.code-block-caption,article p.caption,table>caption{font-size:var(--font-size--small);text-align:center}.toctree-wrapper.compound .caption,.toctree-wrapper.compound :not(.caption)>.caption-text{font-size:var(--font-size--small);margin-bottom:0;text-align:initial;text-transform:uppercase}.toctree-wrapper.compound>ul{margin-bottom:0;margin-top:0}.sig-inline,code.literal{background:var(--color-inline-code-background);border-radius:.2em;font-size:var(--font-size--small--2);padding:.1em .2em}pre.literal-block .sig-inline,pre.literal-block code.literal{font-size:inherit;padding:0}p .sig-inline,p code.literal{border:1px solid var(--color-background-border)}.sig-inline{font-family:var(--font-stack--monospace)}div[class*=" highlight-"],div[class^=highlight-]{display:flex;margin:1em 0}div[class*=" highlight-"] .table-wrapper,div[class^=highlight-] .table-wrapper,pre{margin:0;padding:0}pre{overflow:auto}article[role=main] .highlight pre{line-height:1.5}.highlight pre,pre.literal-block{font-size:var(--code-font-size);padding:.625rem .875rem}pre.literal-block{background-color:var(--color-code-background);border-radius:.2rem;color:var(--color-code-foreground);margin-bottom:1rem;margin-top:1rem}.highlight{border-radius:.2rem;width:100%}.highlight .gp,.highlight span.linenos{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.highlight .hll{display:block;margin-left:-.875rem;margin-right:-.875rem;padding-left:.875rem;padding-right:.875rem}.code-block-caption{background-color:var(--color-code-background);border-bottom:1px solid;border-radius:.25rem;border-bottom-left-radius:0;border-bottom-right-radius:0;border-color:var(--color-background-border);color:var(--color-code-foreground);display:flex;font-weight:300;padding:.625rem .875rem}.code-block-caption+div[class]{margin-top:0}.code-block-caption+div[class]>.highlight{border-top-left-radius:0;border-top-right-radius:0}.highlighttable{display:block;width:100%}.highlighttable tbody{display:block}.highlighttable tr{display:flex}.highlighttable td.linenos{background-color:var(--color-code-background);border-bottom-left-radius:.2rem;border-top-left-radius:.2rem;color:var(--color-code-foreground);padding:.625rem 0 .625rem .875rem}.highlighttable .linenodiv{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;font-size:var(--code-font-size);padding-right:.875rem}.highlighttable td.code{display:block;flex:1;overflow:hidden;padding:0}.highlighttable td.code .highlight{border-bottom-left-radius:0;border-top-left-radius:0}.highlight span.linenos{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;display:inline-block;margin-right:.875rem;padding-left:0;padding-right:.875rem}.footnote-reference{font-size:var(--font-size--small--4);vertical-align:super}dl.footnote.brackets{color:var(--color-foreground-secondary);display:grid;font-size:var(--font-size--small);grid-template-columns:max-content auto}dl.footnote.brackets dt{margin:0}dl.footnote.brackets dt>.fn-backref{margin-left:.25rem}dl.footnote.brackets dt:after{content:":"}dl.footnote.brackets dt .brackets:before{content:"["}dl.footnote.brackets dt .brackets:after{content:"]"}dl.footnote.brackets dd{margin:0;padding:0 1rem}aside.footnote{color:var(--color-foreground-secondary);font-size:var(--font-size--small)}aside.footnote>span,div.citation>span{float:left;font-weight:500;padding-right:.25rem}aside.footnote>:not(span),div.citation>p{margin-left:2rem}img{box-sizing:border-box;height:auto;max-width:100%}article .figure,article figure{border-radius:.2rem;margin:0}article .figure :last-child,article figure :last-child{margin-bottom:0}article .align-left{clear:left;float:left;margin:0 1rem 1rem}article .align-right{clear:right;float:right;margin:0 1rem 1rem}article .align-center,article .align-default{display:block;margin-left:auto;margin-right:auto;text-align:center}article table.align-default{display:table;text-align:initial}.domainindex-jumpbox,.genindex-jumpbox{border-bottom:1px solid var(--color-background-border);border-top:1px solid var(--color-background-border);padding:.25rem}.domainindex-section h2,.genindex-section h2{margin-bottom:.5rem;margin-top:.75rem}.domainindex-section ul,.genindex-section ul{margin-bottom:0;margin-top:0}ol,ul{margin-bottom:1rem;margin-top:1rem;padding-left:1.2rem}ol li>p:first-child,ul li>p:first-child{margin-bottom:.25rem;margin-top:.25rem}ol li>p:last-child,ul li>p:last-child{margin-top:.25rem}ol li>ol,ol li>ul,ul li>ol,ul li>ul{margin-bottom:.5rem;margin-top:.5rem}ol.arabic{list-style:decimal}ol.loweralpha{list-style:lower-alpha}ol.upperalpha{list-style:upper-alpha}ol.lowerroman{list-style:lower-roman}ol.upperroman{list-style:upper-roman}.simple li>ol,.simple li>ul,.toctree-wrapper li>ol,.toctree-wrapper li>ul{margin-bottom:0;margin-top:0}.field-list dt,.option-list dt,dl.footnote dt,dl.glossary dt,dl.simple dt,dl:not([class]) dt{font-weight:500;margin-top:.25rem}.field-list dt+dt,.option-list dt+dt,dl.footnote dt+dt,dl.glossary dt+dt,dl.simple dt+dt,dl:not([class]) dt+dt{margin-top:0}.field-list dt .classifier:before,.option-list dt .classifier:before,dl.footnote dt .classifier:before,dl.glossary dt .classifier:before,dl.simple dt .classifier:before,dl:not([class]) dt .classifier:before{content:":";margin-left:.2rem;margin-right:.2rem}.field-list dd ul,.field-list dd>p:first-child,.option-list dd ul,.option-list dd>p:first-child,dl.footnote dd ul,dl.footnote dd>p:first-child,dl.glossary dd ul,dl.glossary dd>p:first-child,dl.simple dd ul,dl.simple dd>p:first-child,dl:not([class]) dd ul,dl:not([class]) dd>p:first-child{margin-top:.125rem}.field-list dd ul,.option-list dd ul,dl.footnote dd ul,dl.glossary dd ul,dl.simple dd ul,dl:not([class]) dd ul{margin-bottom:.125rem}.math-wrapper{overflow-x:auto;width:100%}div.math{position:relative;text-align:center}div.math .headerlink,div.math:focus .headerlink{display:none}div.math:hover .headerlink{display:inline-block}div.math span.eqno{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);z-index:1}abbr[title]{cursor:help}.problematic{color:var(--color-problematic)}kbd:not(.compound){background-color:var(--color-background-secondary);border:1px solid var(--color-foreground-border);border-radius:.2rem;box-shadow:0 .0625rem 0 rgba(0,0,0,.2),inset 0 0 0 .125rem var(--color-background-primary);color:var(--color-foreground-primary);display:inline-block;font-size:var(--font-size--small--3);margin:0 .2rem;padding:0 .2rem;vertical-align:text-bottom}blockquote{background:var(--color-background-secondary);border-left:4px solid var(--color-background-border);margin-left:0;margin-right:0;padding:.5rem 1rem}blockquote .attribution{font-weight:600;text-align:right}blockquote.highlights,blockquote.pull-quote{font-size:1.25em}blockquote.epigraph,blockquote.pull-quote{border-left-width:0;border-radius:.5rem}blockquote.highlights{background:transparent;border-left-width:0}p .reference img{vertical-align:middle}p.rubric{font-size:1.125em;font-weight:700;line-height:1.25}dd p.rubric{font-size:var(--font-size--small);font-weight:inherit;line-height:inherit;text-transform:uppercase}article .sidebar{background-color:var(--color-background-secondary);border:1px solid var(--color-background-border);border-radius:.2rem;clear:right;float:right;margin-left:1rem;margin-right:0;width:30%}article .sidebar>*{padding-left:1rem;padding-right:1rem}article .sidebar>ol,article .sidebar>ul{padding-left:2.2rem}article .sidebar .sidebar-title{border-bottom:1px solid var(--color-background-border);font-weight:500;margin:0;padding:.5rem 1rem}[role=main] .table-wrapper.container{margin-bottom:.5rem;margin-top:1rem;overflow-x:auto;padding:.2rem .2rem .75rem;width:100%}table.docutils{border-collapse:collapse;border-radius:.2rem;border-spacing:0;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)}table.docutils th{background:var(--color-table-header-background)}table.docutils td,table.docutils th{border-bottom:1px solid var(--color-table-border);border-left:1px solid var(--color-table-border);border-right:1px solid var(--color-table-border);padding:0 .25rem}table.docutils td p,table.docutils th p{margin:.25rem}table.docutils td:first-child,table.docutils th:first-child{border-left:none}table.docutils td:last-child,table.docutils th:last-child{border-right:none}table.docutils td.text-left,table.docutils th.text-left{text-align:left}table.docutils td.text-right,table.docutils th.text-right{text-align:right}table.docutils td.text-center,table.docutils th.text-center{text-align:center}:target{scroll-margin-top:2.5rem}@media(max-width:67em){:target{scroll-margin-top:calc(2.5rem + var(--header-height))}section>span:target{scroll-margin-top:calc(2.8rem + var(--header-height))}}.headerlink{font-weight:100;-webkit-user-select:none;-moz-user-select:none;user-select:none}.code-block-caption>.headerlink,dl dt>.headerlink,figcaption p>.headerlink,h1>.headerlink,h2>.headerlink,h3>.headerlink,h4>.headerlink,h5>.headerlink,h6>.headerlink,p.caption>.headerlink,table>caption>.headerlink{margin-left:.5rem;visibility:hidden}.code-block-caption:hover>.headerlink,dl dt:hover>.headerlink,figcaption p:hover>.headerlink,h1:hover>.headerlink,h2:hover>.headerlink,h3:hover>.headerlink,h4:hover>.headerlink,h5:hover>.headerlink,h6:hover>.headerlink,p.caption:hover>.headerlink,table>caption:hover>.headerlink{visibility:visible}.code-block-caption>.toc-backref,dl dt>.toc-backref,figcaption p>.toc-backref,h1>.toc-backref,h2>.toc-backref,h3>.toc-backref,h4>.toc-backref,h5>.toc-backref,h6>.toc-backref,p.caption>.toc-backref,table>caption>.toc-backref{color:inherit;text-decoration-line:none}figure:hover>figcaption>p>.headerlink,table:hover>caption>.headerlink{visibility:visible}:target>h1:first-of-type,:target>h2:first-of-type,:target>h3:first-of-type,:target>h4:first-of-type,:target>h5:first-of-type,:target>h6:first-of-type,span:target~h1:first-of-type,span:target~h2:first-of-type,span:target~h3:first-of-type,span:target~h4:first-of-type,span:target~h5:first-of-type,span:target~h6:first-of-type{background-color:var(--color-highlight-on-target)}:target>h1:first-of-type code.literal,:target>h2:first-of-type code.literal,:target>h3:first-of-type code.literal,:target>h4:first-of-type code.literal,:target>h5:first-of-type code.literal,:target>h6:first-of-type code.literal,span:target~h1:first-of-type code.literal,span:target~h2:first-of-type code.literal,span:target~h3:first-of-type code.literal,span:target~h4:first-of-type code.literal,span:target~h5:first-of-type code.literal,span:target~h6:first-of-type code.literal{background-color:transparent}.literal-block-wrapper:target .code-block-caption,.this-will-duplicate-information-and-it-is-still-useful-here li :target,figure:target,table:target>caption{background-color:var(--color-highlight-on-target)}dt:target{background-color:var(--color-highlight-on-target)!important}.footnote-reference:target,.footnote>dt:target+dd{background-color:var(--color-highlight-on-target)}.guilabel{background-color:var(--color-guilabel-background);border:1px solid var(--color-guilabel-border);border-radius:.5em;color:var(--color-guilabel-text);font-size:.9em;padding:0 .3em}footer{display:flex;flex-direction:column;font-size:var(--font-size--small);margin-top:2rem}.bottom-of-page{align-items:center;border-top:1px solid var(--color-background-border);color:var(--color-foreground-secondary);display:flex;justify-content:space-between;line-height:1.5;margin-top:1rem;padding-bottom:1rem;padding-top:1rem}@media(max-width:46em){.bottom-of-page{flex-direction:column-reverse;gap:.25rem;text-align:center}}.bottom-of-page .left-details{font-size:var(--font-size--small)}.bottom-of-page .right-details{display:flex;flex-direction:column;gap:.25rem;text-align:right}.bottom-of-page .icons{display:flex;font-size:1rem;gap:.25rem;justify-content:flex-end}.bottom-of-page .icons a{text-decoration:none}.bottom-of-page .icons img,.bottom-of-page .icons svg{font-size:1.125rem;height:1em;width:1em}.related-pages a{align-items:center;display:flex;text-decoration:none}.related-pages a:hover .page-info .title{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}.related-pages a svg.furo-related-icon,.related-pages a svg.furo-related-icon>use{color:var(--color-foreground-border);flex-shrink:0;height:.75rem;margin:0 .5rem;width:.75rem}.related-pages a.next-page{clear:right;float:right;max-width:50%;text-align:right}.related-pages a.prev-page{clear:left;float:left;max-width:50%}.related-pages a.prev-page svg{transform:rotate(180deg)}.page-info{display:flex;flex-direction:column;overflow-wrap:anywhere}.next-page .page-info{align-items:flex-end}.page-info .context{align-items:center;color:var(--color-foreground-muted);display:flex;font-size:var(--font-size--small);padding-bottom:.1rem;text-decoration:none}ul.search{list-style:none;padding-left:0}ul.search li{border-bottom:1px solid var(--color-background-border);padding:1rem 0}[role=main] .highlighted{background-color:var(--color-highlighted-background);color:var(--color-highlighted-text)}.sidebar-brand{display:flex;flex-direction:column;flex-shrink:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none}.sidebar-brand-text{color:var(--color-sidebar-brand-text);font-size:1.5rem;overflow-wrap:break-word}.sidebar-brand-text,.sidebar-logo-container{margin:var(--sidebar-item-spacing-vertical) 0}.sidebar-logo{display:block;margin:0 auto;max-width:100%}.sidebar-search-container{align-items:center;background:var(--color-sidebar-search-background);display:flex;margin-top:var(--sidebar-search-space-above);position:relative}.sidebar-search-container:focus-within,.sidebar-search-container:hover{background:var(--color-sidebar-search-background--focus)}.sidebar-search-container:before{background-color:var(--color-sidebar-search-icon);content:"";height:var(--sidebar-search-icon-size);left:var(--sidebar-item-spacing-horizontal);-webkit-mask-image:var(--icon-search);mask-image:var(--icon-search);position:absolute;width:var(--sidebar-search-icon-size)}.sidebar-search{background:transparent;border:none;border-bottom:1px solid var(--color-sidebar-search-border);border-top:1px solid var(--color-sidebar-search-border);box-sizing:border-box;color:var(--color-sidebar-search-foreground);padding:var(--sidebar-search-input-spacing-vertical) var(--sidebar-search-input-spacing-horizontal) var(--sidebar-search-input-spacing-vertical) calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size));width:100%;z-index:10}.sidebar-search:focus{outline:none}.sidebar-search::-moz-placeholder{font-size:var(--sidebar-search-input-font-size)}.sidebar-search::placeholder{font-size:var(--sidebar-search-input-font-size)}#searchbox .highlight-link{margin:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0;text-align:center}#searchbox .highlight-link a{color:var(--color-sidebar-search-icon);font-size:var(--font-size--small--2)}.sidebar-tree{font-size:var(--sidebar-item-font-size);margin-bottom:var(--sidebar-item-spacing-vertical);margin-top:var(--sidebar-tree-space-above)}.sidebar-tree ul{display:flex;flex-direction:column;list-style:none;margin-bottom:0;margin-top:0;padding:0}.sidebar-tree li{margin:0;position:relative}.sidebar-tree li>ul{margin-left:var(--sidebar-item-spacing-horizontal)}.sidebar-tree .icon,.sidebar-tree .reference{color:var(--color-sidebar-link-text)}.sidebar-tree .reference{box-sizing:border-box;display:inline-block;height:100%;line-height:var(--sidebar-item-line-height);overflow-wrap:anywhere;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none;width:100%}.sidebar-tree .reference:hover{background:var(--color-sidebar-item-background--hover);color:var(--color-sidebar-link-text)}.sidebar-tree .reference.external:after{color:var(--color-sidebar-link-text);content:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23607d8b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' viewBox='0 0 24 24'%3E%3Cpath stroke='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M11 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-5M10 14 20 4M15 4h5v5'/%3E%3C/svg%3E");margin:0 .25rem;vertical-align:middle}.sidebar-tree .current-page>.reference{font-weight:700}.sidebar-tree label{align-items:center;cursor:pointer;display:flex;height:var(--sidebar-item-height);justify-content:center;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:var(--sidebar-expander-width)}.sidebar-tree .caption,.sidebar-tree :not(.caption)>.caption-text{color:var(--color-sidebar-caption-text);font-size:var(--sidebar-caption-font-size);font-weight:700;margin:var(--sidebar-caption-space-above) 0 0 0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-transform:uppercase}.sidebar-tree li.has-children>.reference{padding-right:var(--sidebar-expander-width)}.sidebar-tree .toctree-l1>.reference,.sidebar-tree .toctree-l1>label .icon{color:var(--color-sidebar-link-text--top-level)}.sidebar-tree label{background:var(--color-sidebar-item-expander-background)}.sidebar-tree label:hover{background:var(--color-sidebar-item-expander-background--hover)}.sidebar-tree .current>.reference{background:var(--color-sidebar-item-background--current)}.sidebar-tree .current>.reference:hover{background:var(--color-sidebar-item-background--hover)}.toctree-checkbox{display:none;position:absolute}.toctree-checkbox~ul{display:none}.toctree-checkbox~label .icon svg{transform:rotate(90deg)}.toctree-checkbox:checked~ul{display:block}.toctree-checkbox:checked~label .icon svg{transform:rotate(-90deg)}.toc-title-container{padding:var(--toc-title-padding);padding-top:var(--toc-spacing-vertical)}.toc-title{color:var(--color-toc-title-text);font-size:var(--toc-title-font-size);padding-left:var(--toc-spacing-horizontal);text-transform:uppercase}.no-toc{display:none}.toc-tree-container{padding-bottom:var(--toc-spacing-vertical)}.toc-tree{border-left:1px solid var(--color-background-border);font-size:var(--toc-font-size);line-height:1.3;padding-left:calc(var(--toc-spacing-horizontal) - var(--toc-item-spacing-horizontal))}.toc-tree>ul>li:first-child{padding-top:0}.toc-tree>ul>li:first-child>ul{padding-left:0}.toc-tree>ul>li:first-child>a{display:none}.toc-tree ul{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:var(--toc-item-spacing-horizontal)}.toc-tree li{padding-top:var(--toc-item-spacing-vertical)}.toc-tree li.scroll-current>.reference{color:var(--color-toc-item-text--active);font-weight:700}.toc-tree a.reference{color:var(--color-toc-item-text);overflow-wrap:anywhere;text-decoration:none}.toc-scroll{max-height:100vh;overflow-y:scroll}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here){background:rgba(255,0,0,.25);color:var(--color-problematic)}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here):before{content:"ERROR: Adding a table of contents in Furo-based documentation is unnecessary, and does not work well with existing styling. Add a 'this-will-duplicate-information-and-it-is-still-useful-here' class, if you want an escape hatch."}.text-align\:left>p{text-align:left}.text-align\:center>p{text-align:center}.text-align\:right>p{text-align:right} +/*# sourceMappingURL=furo.css.map*/ \ No newline at end of file diff --git a/docs/slidgram/main/admin/attachments.html b/docs/slidgram/main/admin/attachments.html new file mode 100644 index 0000000..3b088fc --- /dev/null +++ b/docs/slidgram/main/admin/attachments.html @@ -0,0 +1,475 @@ + + + + + + + + + Attachments - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Attachments

+
+

Note

+

Attachments from XMPP to Telegram require no special configuration.

+
+

For slidgram to bridge attachments from Telegram to XMPP, you have two options:

+
    +
  • HTTP Upload: slidgram will use your XMPP server’s upload component (XEP-0363).

  • +
  • No upload: slidgram will copy attachments to a path it can write to. You then need to serve these files from via an HTTP server (eg nginx, +prosody’s http_files, etc.).

  • +
+
+

HTTP Upload

+

slidgram can use the HTTP Upload component (XEP-0363) of your XMPP server, +if you configure it with upload-service=upload.example.org (see Generic slidge config). +In this setting, slidgram will upload files just like any normal user of your server.

+
+

Example 1: prosody’s mod_http_file_share

+

In slidgram’s configuration file, use upload-service=upload.example.org

+
modules_enabled = {
+  -- make sure http_file_share is listed here
+  "http_file_share";
+}
+
+Component "upload.example.org" "http_file_share"
+  -- allow slidgram to use the upload component
+  http_file_share_access = { "telegram.example.org" }
+
+
+

More info: mod_http_file_share.

+
+
+

Example 2: ejabberd mod_http_upload

+

In slidgram’s configuration file, use upload-service=example.org

+

The subdomain’s FQDN (example.org) should be listed under the top level ‘hosts’.

+
hosts:
+  - "example.org"
+
+acl:
+  slidge_acl:
+    server:
+      - "telegram.example.org"
+
+listen:
+  -
+    port: 5443
+    module: ejabberd_http
+    tls: true
+    request_handlers:
+      /upload: mod_http_upload
+
+modules:
+  mod_http_upload:
+    # Any path that ejabberd has read and write access to
+    docroot: /ejabberd/upload
+    put_url: "https://@HOST@:5443/upload"
+    access:
+      - allow: local
+      - allow: slidge_acl
+
+
+

To get more information about component configuration, see ejabberd’s docs.

+
+
+
+

No upload

+

You need to set up no-upload-path to point to a directory, and no-upload-url-prefix to an URL prefix pointing to files in that directory (see Generic slidge config for more detail). +Example: no-upload-path=/var/www/slidgram-attachments/ and no-upload-url-prefix=https://example.org/slidgram/ means that /var/www/slidgram-attachments/some-image.jpg is accessible at https://example.org/slidgram/some-image.jpg

+

Make sure that no-upload-path is writeable by slidgram and readable by +your HTTP server. You may use no-upload-file-read-others=true to do that easily, +but you might want to restrict which users can read this directory.

+
+

Warning

+

slidgram will not take care of removing old files, so you should set up a cronjob, +a systemd timer, or something similar, to regularly delete files, eg. +find . -mtime +7 -delete && find . -depth -type d -empty -delete +to clean up files older than a week.

+
+

For the following examples, in slidgram’s config, +you would have no-upload-path=/var/lib/slidgram/attachments.

+
+

Example 1: prosody’s http_files

+

Here, no-upload-url-prefix would be https://example.org:5281/files/, +as per the mod_http_files documentation.

+
modules_enabled = {
+  -- make sure http_files is listed here
+  "http_files";
+}
+
+-- Must be the same value as slidgram's no-upload-path
+http_files_dir = "/var/lib/slidgram/attachments"
+
+
+
+
+

Example 2: nginx

+

Here, no-upload-url-prefix would be https://example.org/slidgram/.

+
server {
+  listen 80;
+  server_name example.org;
+  root /var/www/html;  # if you already have nginx serving files…
+
+  # the section below is for slidgram
+  location /slidgram {
+    #  Must be the same value as slidgram's no-upload-path
+    alias /var/lib/slidgram/attachments/;
+  }
+}
+
+
+

See the nginx docs for more info.

+
+
+
+

Next steps

+

To make your slidgram install top notch, set up its privileges.

+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/admin/config.html b/docs/slidgram/main/admin/config.html new file mode 100644 index 0000000..fd728a1 --- /dev/null +++ b/docs/slidgram/main/admin/config.html @@ -0,0 +1,677 @@ + + + + + + + + + Configuration - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Configuration

+

Both +slidgram-specific options and +slidge generic options +can be set using +config text file(s), +command-line arguments, +or environment variables.

+
+

Config files

+
+

Location

+

By default, slidgram uses all config files found in /etc/slidge/conf.d/*.conf. +You can change this using the SLIDGE_CONF_DIR env var, eg +SLIDGE_CONF_DIR=/path/dir1:/path/dir2:/path/dir3.

+

We recommend using /etc/slidge/conf.d/common.conf file to set +the options common to several slidge-based gateways +(eg, attachment handling, logging options, etc.), +then use a slidgram-specific dedicated config file, eg +/etc/slidge/slidgram.conf.

+

Point to this specific file using the -c command line argument when +launching slidgram.

+
slidgram -c /etc/slidge/slidgram.conf
+
+
+
+
+

Syntax

+

Config files are simple text files with key=value entries.

+
some-option=some-value
+some-other-option=some-other-value
+
+
+
+
+
+

Command-line arguments

+

To pass options as command-line arguments, prepend their name with --.

+
slidgram --some-option=some-value --some-other-option=some-other-value
+
+
+
+
+

Environment variables

+

To pass options as environment variables:

+ +
SLIDGE_SOME_GENERIC_OPTION=some-value
+SLIDGRAM_SOME_SPECIFIC_OPTION=some-other-value
+
+
+
+
+

slidgram-specific config

+

slidgram provides the instance-wide options displayed in the table below.

+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedefault

help

api-hashNone

Telegram app api_hash, obtained at https://my.telegram.org/apps If you dont set it, users will have to enter their own on registration.

api-idNone

Telegram app api_id, obtained at https://my.telegram.org/apps If you dont set it, users will have to enter their own on registration.

attachment-max-size10485760

Maximum file size (in bytes) to download from telegram automatically/

big-avatarsFalse

Fetch contact avatars in high-resolution (640x640) instead of the default 160x160. NB: slidge core main config AVATAR_SIZE still applies.

group-history-maximum-messages50

The number of messages to fetch from a group history. These messages and their attachments will be fetched on slidge startup.

registration-auth-code-timeout60

On registration, users will be prompted for a 2FA code they receive on other telegram clients.

+
+
+
+

Generic slidge config

+
+

Note

+

The following options are for slidge version 0.5.0a2.dev2+g8ca420ef0. +Depending on how you installed slidgram, you might have a different version of slidge. +Use slidgram --help for the exact list of options you can use.

+
+

Slidge provides the generic instance-wide options displayed in the table below. +They may not all have an effect on slidgram’s behaviour.

+
+

Mandatory settings

+
+ + + + + + + + + + + + + + + + + +
namedefault

help

secretunset

The gateway component’s secret (required to connect to the XMPP server)

jidunset

The gateway component’s JID

+
+
+
+

Basic configuration

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedefault

help

admins()

JIDs of the gateway admins

mam-max-days7

Maximum number of days for group archive retention.

port5347

The XMPP server’s port for incoming component connections

serverlocalhost

The XMPP server’s host name. Defaults to localhost, which is the standard way of running slidge, on the same host as the XMPP server. The ‘Jabber Component Protocol’ (XEP-0114) does not mention encryption, so you *should* provide encryption another way, eg via port forwarding, if you change this.

legacy-moduleunset

Importable python module containing (at least) a BaseGateway and a LegacySession subclass. NB: this is not needed if you use a gateway-specific entrypoint, e.g., `slidgram` or `python -m slidgram`.

home-dirinferred

Directory where slidge will writes it persistent data and cache. Defaults to /var/lib/slidge/${SLIDGE_JID}.

user-jid-validatorinferred

Regular expression to restrict users that can register to the gateway, by JID. Defaults to .*@${INFERRED_SERVER}. INFERRED_SERVER is derived for the gateway JID, by removing whatever is before the first encountered dot in it. Example: if slidge’s JID=slidge.example.org, INFERRED_SERVER=example.org.

+
+
+
+

Attachments

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedefault

help

attachment-maximum-file-name-length200

Some legacy network provide ridiculously long filenames, strip above this limit, preserving suffix.

convert-stickersFalse

Convert lottie vector stickers (from the legacy side) to webp animations.

fix-filename-suffix-mime-typeFalse

Fix the Filename suffix based on the Mime Type of the file. Some clients (eg Conversations) may not inline files that have a wrong suffix for the MIME Type. Therefore the MIME Type of the file is checked, if the suffix is not valid for that MIME Type, a valid one will be picked.

no-upload-file-read-othersFalse

After writing a file in NO_UPLOAD_PATH, change its permission so that ‘others’ can read it.

no-upload-pathNone

Instead of using the XMPP server’s HTTP upload component, copy files to this dir. You need to set NO_UPLOAD_URL_PREFIX too if you use this option, and configure an web server to serve files in this dir.

no-upload-url-prefixNone

Base URL that servers files in the dir set in the no-upload-path option, eg https://example.com:666/slidge-attachments/

upload-requesterNone

Set which JID should request the upload slots. Defaults to the user’s JID if IQ/get privileges granted for the ‘urn:xmpp:http:upload:0’ namespace; the component JID otherwise.

upload-serviceNone

JID of an HTTP upload service the gateway can use. This is optional, as it should be automatically determined via servicediscovery.

upload-url-prefixNone

This is an optional setting to make sure the URL of your upload service is never leaked to the legacy network in bodies of messages. This can happen under rare circumstances and/or bugs,when replying to an attachment. Set this to the common prefix of the public URL your attachments get, eg https://upload.example.org:5281/

use-attachment-original-urlsFalse

For legacy plugins in which attachments are publicly downloadable URLs, let XMPP clients directly download them from this URL. Note that this will probably leak your client IP to the legacy network.

+
+
+
+

Logging

+
+ + + + + + + + + + + + + + + + + +
namedefault

help

log-fileNone

Log to a file instead of stdout/err

log-format%(levelname)s:%(name)s:%(message)s

Optionally, a format string for logging messages. Refer to https://docs.python.org/3/library/logging.html#logrecord-attributes for available options.

+
+
+
+

Advanced settings

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedefault

help

avatar-resampling-threads2

Number of additional threads to use for avatar resampling. Even in a single-core context, this makes avatar resampling non-blocking.

avatar-size200

Maximum image size (width and height), image ratio will be preserved

component-nameNone

Overrides the default component name with a custom one. This is seen in service discovery and as the nickname of the component in chat windows.

dev-modeFalse

Enables an interactive python shell via chat commands, for admins.Not safe to use in prod, but great during dev.

ignore-delay-threshold300

Threshold, in seconds, below which the <delay> information is stripped out of emitted stanzas.

partial-registration-timeout3600

Timeout before registration and login. Only useful for legacy networks where a single step registration process is not enough.

qr-timeout60

Timeout for QR code flashing confirmation.

strip-leading-emoji-adhocFalse

Strip the leading emoji in ad-hoc command names, if present, in case you are a emoji-hater.

welcome-messageNone

Overrides the default welcome message received by newly registered users.

db-urlinferred

Database URL, see <https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls>. Defaults to `sqlite:///${HOME_DIR}/slidge.sqlite`. SQLite is *highly recommended*, other backends such as postgresql may work by chance but are much less tested.

+
+
+
+
+

Advanced logging configuration

+

To customize the output of the slidge, you can use the command line argument --log-config +to specify a logging configuration file.

+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/admin/examples/index.html b/docs/slidgram/main/admin/examples/index.html new file mode 100644 index 0000000..ed53f05 --- /dev/null +++ b/docs/slidgram/main/admin/examples/index.html @@ -0,0 +1,506 @@ + + + + + + + + + XMPP server configs examples - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

XMPP server configs examples

+
+

Note

+

These examples are not meant to be complete, but rather show the relevant +parts for slidge.

+
+
+

prosody/upload

+
 1modules_enabled = {
+ 2  -- [...]
+ 3  "http_file_share"; -- for attachments with the "upload" option
+ 4  "privilege"; -- for roster sync and 'legacy carbons'
+ 5}
+ 6
+ 7local _privileges = {
+ 8    roster = "both";
+ 9    message = "outgoing";
+10    iq = {
+11      ["http://jabber.org/protocol/pubsub"] = "both";
+12      ["http://jabber.org/protocol/pubsub#owner"] = "set";
+13      ["urn:xmpp:http:upload:0"] = "get";
+14    };
+15}
+16
+17VirtualHost "example.org"
+18  -- for roster sync and 'legacy carbons'
+19  privileged_entities = {
+20    ["telegram.example.org"] =_privileges,
+21    ["other-walled-garden.example.org"] = _privileges,
+22    -- repeat for other slidge plugins…
+23  }
+24
+25Component "telegram.example.org"
+26  component_secret = "secret"
+27  modules_enabled = {"privilege"}
+28
+29Component "other-walled-garden.example.org"
+30  component_secret = "some-other-secret"
+31  modules_enabled = {"privilege"}
+32
+33-- for attachments with the "upload" option
+34-- in telegram's config: upload-service=upload.example.org
+35Component "upload.example.org" "http_file_share"
+36    server_user_role = "prosody:registered"
+37    -- alternatively, you can be more specific with:
+38    -- http_file_share_access = { "telegram.example.org", "other-walled-garden.example.org" }
+
+
+
+
+

prosody/no-upload

+
 1modules_enabled = {
+ 2  -- [...]
+ 3  "http_files"; -- to serve "no upload" attachments
+ 4  "privilege"; -- for roster sync and 'legacy carbons'
+ 5}
+ 6
+ 7-- in slidge's config: no-upload-path=/var/lib/slidge/attachments
+ 8http_files_dir = "/var/lib/slidge/attachments"
+ 9
+10local _privileges = {
+11    roster = "both";
+12    message = "outgoing";
+13    iq = {
+14      ["http://jabber.org/protocol/pubsub"] = "both";
+15      ["http://jabber.org/protocol/pubsub#owner"] = "set";
+16    };
+17}
+18
+19VirtualHost "example.org"
+20  -- for roster sync and 'legacy carbons'
+21  privileged_entities = {
+22    ["telegram.example.org"] =_privileges,
+23    ["other-walled-garden.example.org"] = _privileges,
+24    -- repeat for other slidge plugins…
+25  }
+26
+27Component "telegram.example.org"
+28  component_secret = "secret"
+29  modules_enabled = {"privilege"}
+30
+31Component "other-walled-garden.example.org"
+32  component_secret = "some-other-secret"
+33  modules_enabled = {"privilege"}
+
+
+
+
+

ejabberd/upload

+
+

Note

+

This example does not cover the No upload option for attachments (see Configuration). +For ‘no upload’ with ejabberd, you need an external HTTP server, eg +Example 2: nginx.

+
+
 1listen:
+ 2  -
+ 3    ip: 127.0.0.1
+ 4    port: 5347
+ 5    module: ejabberd_service
+ 6    # The next line is required if you're settings up multiple bridges
+ 7    global_routes: false
+ 8    hosts:
+ 9      - "superduper.example.org":
+10          password: secret
+11      - "other-walled-garden.example.org":
+12          password: some-other-secret
+13      # repeat for other slidge plugins…
+14
+15  # HTTP upload service (XEP-0363)
+16  -
+17    port: 5443
+18    module: ejabberd_http
+19    tls: true
+20    request_handlers:
+21      /upload: mod_http_upload
+22
+23acl:
+24  slidge_acl:
+25    server:
+26      - "superduper.example.org"
+27      - "other-walled-garden.example.org"
+28      # repeat for other slidge plugins you added in the listen section above
+29
+30access_rules:
+31  slidge_rule:
+32    - allow: slidge_acl
+33
+34modules:
+35  mod_http_upload:
+36    # A path that ejabberd has Read and Write access to
+37    docroot: /ejabberd/upload
+38    put_url: "https://@HOST@:5443/upload"
+39    access:
+40      - allow: local
+41      - allow: slidge_acl
+42
+43  # for roster auto-fill and "carbons from legacy apps"
+44  # (broken in ejabberd when this was written, hopefully fixed since)
+45  mod_privilege:
+46    roster:
+47      both: slidge_rule
+48    message:
+49      outgoing: slidge_rule
+50    iq:
+51      "http://jabber.org/protocol/pubsub":
+52        both: slidge_rule
+53      "http://jabber.org/protocol/pubsub#owner":
+54        set: slidge_rule
+55      "urn:xmpp:http:upload:0":
+56        get: slidge_rule
+57  mod_roster:
+58    versioning: true
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/admin/index.html b/docs/slidgram/main/admin/index.html new file mode 100644 index 0000000..7d6550e --- /dev/null +++ b/docs/slidgram/main/admin/index.html @@ -0,0 +1,404 @@ + + + + + + + + + For admins - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ + +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/admin/install.html b/docs/slidgram/main/admin/install.html new file mode 100644 index 0000000..fc8e0df --- /dev/null +++ b/docs/slidgram/main/admin/install.html @@ -0,0 +1,416 @@ + + + + + + + + + Installation - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Installation

+
+

Containers

+

A container is built on every push to the git repository and uploaded to the codeberg package +registry.

+
docker run codeberg.org/slidge/slidgram:latest  # works with podman too
+
+
+

Use the :latest tag for the latest release, :vX.X.X for release +X.X.X, and :main for the bleeding edge.

+

For data persistence, mount a writeable directory into /var/lib/slidge. Inside the container, +slidgram runs as the slidge user with UID/GID 10000/10000.

+

slidgram must be able to reach your XMPP server, so set up networking accordingly.

+
+
+

Python packages

+PyPI package version + +

slidgram is available on the python package index (PyPI).

+

If you are not familiar with python packaging, we recommend using pipx to +set up slidgram and its dependencies, isolated from the rest of your system.

+
# for the latest stable release published to PyPI, if any
+pipx install slidgram
+
+slidgram --help
+
+
+

Bleeding edge versions are also available on codeberg’s python index.

+
# for the bleeding edge
+pipx install slidgram \
+    --pip-args='--extra-index-url https://codeberg.org/api/packages/slidge/pypi/simple/ --pre'
+
+
+
+
+

Unofficial debian package

+

If you are using debian you might be interested in installing the +slidge (unofficial) debian +package which bundles slidgram +along with other slidge-based XMPP gateways.

+

Follow the instructions in the repository README. In short:

+
    +
  • Configure your XMPP ;

  • +
  • edit /etc/slidge/conf.d/common.conf;

  • +
  • edit /etc/slidge/telegram.conf;

  • +
  • run sudo systemctl start slidge@telegram;

  • +
  • watch the logs with sudo journalctl -u slidge@telegram -f.

  • +
+
+
+

NetBSD-specific instructions

+

It is possible to install slidgram on bare metal or in a sandbox.

+

First, install the necessary build dependencies:

+
pkgin install py313-pip py313-setuptools py313-setuptools-rust py313-wheel rust rust-bin gcc14
+
+
+

Then, optionally, install the dependencies available with pkgin:

+
pkgin install py313-uvloop py313-aiohttp py313-alembic py313-Pillow py313-configargparse py313-defusedxml py313-qrcode py313-sqlalchemy py313-aiohappyeyeballs py313-aiosignal py313-attrs py313-frozenlist py313-multidict py313-propcache py313-yarl py313-aiodns py313-brotli py313-mako py313-greenlet py313-idna py313-cffi
+
+
+

After this, install with pip:

+
PATH="$PATH:/usr/pkg/gcc14/bin" pip3.13 install slidgram
+
+
+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/admin/privileges.html b/docs/slidgram/main/admin/privileges.html new file mode 100644 index 0000000..eb00f58 --- /dev/null +++ b/docs/slidgram/main/admin/privileges.html @@ -0,0 +1,441 @@ + + + + + + + + + Privileges - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Privileges

+
+

Note

+

“XEP-0356: Privileged Entity” only works for XMPP users using the same server as slidgram, e.g., eve@example.org on telegram.example.org.

+
+

Setting up slidgram as a privileged entity (XEP-0356) is optional, but nice to have. +It improves user experience with a few “cherry on top” features. +By configuring your XMPP server such that slidgram is a privileged entity, slidgram can:

+
    +
  • automatically add/remove “puppet contacts” from the XMPP roster of slidgram users,

  • +
  • reflect on the XMPP side messages sent by users via official Telegram apps,

  • +
  • (if using HTTP Upload for attachments from Telegram to XMPP) request upload slots on behalf of slidgram users, +respecting any quota, retention, permission, or other policy set at the upload component level,

  • +
  • synchronize other actions done via official Telegram apps, such as marking messages as read, using emoji reactions, +retracting messages, sending files…

  • +
  • automatically add XMPP bookmarks (XEP-0402) for MUCs (XEP-0045).

  • +
+
+

Server-specific instructions

+
+

Prosŏdy

+

Install the mod_privilege +community module with:

+
prosodyctl install --server=https://modules.prosody.im/rocks/ mod_privilege
+
+
+

In prosody.cfg.lua, add mod_privilege to the modules_enabled list, and +declare slidgram privileges in the appropriate virtualhost block:

+
local _privileges = {
+  roster = "both";       -- for adding/removing contacts from the users' rosters
+  message = "outgoing";  -- for reflecting messages sent by the user themselve from official Telegram apps
+  iq = {
+    ["http://jabber.org/protocol/pubsub"] = "both";      -- for PEP Bookmarks
+    ["http://jabber.org/protocol/pubsub#owner"] = "set"; -- for Message Display Synchronization
+    ["urn:xmpp:http:upload:0"] = "get";                  -- for HTTP Upload on behalf of users
+  }
+};
+
+VirtualHost "example.org"
+  privileged_entities = {
+    ["telegram.example.org"] = _privileges;
+  }
+
+Component "telegram.example.org"
+  modules_enabled = {"privilege"}
+
+
+

Then either restart the prosody server, or reload the config. +You might need to use +mod_reload_component, +and activate/deactivate hosts +for all changes to be taken into account +(restarting prosody is the easiest way to go).

+
+
+

ejabberd

+
acl:
+  slidge_acl:
+    server:
+      - "telegram.example.org"
+
+access_rules:
+  slidge_rule:
+    - allow: slidge_acl
+
+modules:
+  mod_privilege:
+    roster:
+      both: slidge_rule
+    message:
+      outgoing: slidge_rule
+    iq:
+      "http://jabber.org/protocol/pubsub":
+        both: slidge_rule
+      "http://jabber.org/protocol/pubsub#owner":
+        set: slidge_rule
+      "urn:xmpp:http:upload:0":
+        get: slidge_rule
+  mod_roster:
+    versioning: true
+
+
+
+
+
+

Next step

+

Learn about about slidgram’s configuration to tune its behaviour to your liking.

+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/admin/quickstart.html b/docs/slidgram/main/admin/quickstart.html new file mode 100644 index 0000000..27fb288 --- /dev/null +++ b/docs/slidgram/main/admin/quickstart.html @@ -0,0 +1,403 @@ + + + + + + + + + Quick start - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Quick start

+
+

Setup an XMPP server component

+

slidgram uses the Jabber Component Protocol to +connect to an XMPP server. +slidgram itself needs to have a JID without a local part, such as telegram.example.org. +In a typical deployment, slidgram runs on the same host as the XMPP server and connects to it via localhost. +This requires adequate configuration of the XMPP server, and depends on the XMPP server software you are using.

+

This documentation explains how to do that for +prosody +and ejabberd. +It might be outdated and you may want to check the official, up-to-date documentation of the XMPP server you are using. +If you know how to set up slidge with other XMPP servers, please contribute to the docs. ;-)

+
+

Prosŏdy

+

Add a component block below the appropriate virtualhost in prosody.cfg.lua

+
Component "telegram.example.org"
+  component_secret = "some-secret-string"
+
+
+
+
+

ejabberd

+
listen:
+  -
+    ip: 127.0.0.1
+    port: 5347
+    module: ejabberd_service
+    hosts:
+      telegram.example.org:
+        password: some-secret-string
+
+
+
+
+
+

Launch the daemon

+

Start slidgram with:

+
slidgram \
+  --jid telegram.example.org \
+  --secret some-secret-string \
+  --home-dir /somewhere/writable
+
+
+
+
+

Next steps

+

Next, you probably want to set up attachments to support bridging files from Telegram to XMPP.

+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/contributing.html b/docs/slidgram/main/contributing.html new file mode 100644 index 0000000..f31fc9a --- /dev/null +++ b/docs/slidgram/main/contributing.html @@ -0,0 +1,437 @@ + + + + + + + + + Contributing - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Contributing

+

We are really happy to welcome new contributors to slidgram! +Before starting anything, please join our chat room +to say hi and see if anyone is already working on the bug you want to fix or +the feature you want to implement.

+
+

Quickstart

+

To start hacking on slidgram, you need to:

+
    +
  1. Clone the repository: git clone https://codeberg.org/slidge/slidgram.

  2. +
  3. Install the required dependencies. (see below)

  4. +
  5. Spin up a development XMPP server for slidgram to connect to. (see below)

  6. +
+
+

Easy mode: docker-compose

+

The easiest way to achieve 2 and 3 at the same time is to use the provided +docker-compose.yml file. Run docker compose up (or podman-compose up) +in the directory of the repository you just cloned. It will:

+
    +
  • spin up a prosody instance configured for development;

  • +
  • launch slidgram in a dedicated container, with hot code reload.

  • +
+

You will then be able to connect to that prosody using any XMPP client, using +“test@localhost” as JID and “password” as password. We recommended +gajim which lets you start a different profile than your +main profile: gajim -p slidge -c ~/.local/share/gajim-slidge.

+

To avoid having to accept self-signed certificates, you can add prosody’s +certificate to your local store. In debian, you can do that with:

+
# download the certificate
+curl https://codeberg.org/slidge/prosody-dev-container/raw/branch/main/localhost.crt | sudo tee /usr/local/share/ca-certificates/localhost.crt
+# set the right perms
+sudo chmod 600 /usr/local/share/ca-certificates/localhost.crt
+# import it
+sudo update-ca-certificates
+
+
+
+
+

Slightly harder mode: setting up a virtualenv

+

In some situations, developing in containers is not optimal, for instance if +you want to attach an interactive debugger to the slidgram process.

+

slidgram defines its dependencies in a PEP 517-compliant +pyproject.toml file. This means that you can use different tools to set up a +virtualenv with the appropriate dependencies.

+

We recommend using uv which is fast and has some +nice features. By running uv sync --frozen --all-groups --all-extras, a +standard virtualenv will be installed in ./.venv. You can then activate it +with source .venv/bin/activate and launch slidgram --help.

+

NB: you will need to set up a local XMPP server. An easy way to do that is to +use the slidge-prosody-dev container: docker run --network host codeberg.org/slidge/prosody-slidge-dev. +With it you will be able to launch slidgram with +slidgram --jid slidge.localhost --secret secret --debug --home-dir ./persistent.

+
+
+

Hacking on slidge core simultaneously

+

Maybe you will discover that what you want to change, add or fix is not part +of slidgram but part of slidge core. To modify slidge core, first you +need to clone the slidge repo somewhere +on your computer.

+

Let’s assume you cloned slidge in the same root dir as slidgram, +e.g., ~/src/slidgram and ~/src/slidge. +With the docker-compose-based dev setup, you will need to add an additional +mount for the slidge dir, e.g., ../slidge:/build/slidge. If you opted for +the virtualenv solution, you can install slidge in your virtualenv in editable +mode with [uv] pip install -e ../slidge.

+
+
+
+

Guidelines

+

slidgram uses these tools to ensure some level of code quality:

+
    +
  • mypy +for static type checking,

  • +
  • ruff +to detect common python mistakes and enforce a consistent style,

  • +
  • pytest +for automated tests.

  • +
+

Commit messages should be in the form of +conventional commits, with +some additional types defined in ./commitlinx.config.js. +This makes it possible to automatically generate changelogs on releases, it +is worth it! We use git-cliff for this. You don’t have +to install git-cliff locally, the magic happens in CI.

+

We recommended setting up pre-commit to ensure that +these pass for each commit: pre-commit install && pre-commit install --hook-type commit-msg.

+

Make a new git branch, commit your changes, push it to your fork and open a +pull request!

+

NB: we also accept contributions without pull requests. Just push your changes +somewhere and tell us where to pull via the group chat.

+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/genindex.html b/docs/slidgram/main/genindex.html new file mode 100644 index 0000000..e282a2f --- /dev/null +++ b/docs/slidgram/main/genindex.html @@ -0,0 +1,302 @@ + + + + + + + Index - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+ +
+

Index

+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/index.html b/docs/slidgram/main/index.html new file mode 100644 index 0000000..3e75fb0 --- /dev/null +++ b/docs/slidgram/main/index.html @@ -0,0 +1,360 @@ + + + + + + + + + slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/search.html b/docs/slidgram/main/search.html new file mode 100644 index 0000000..27b6966 --- /dev/null +++ b/docs/slidgram/main/search.html @@ -0,0 +1,313 @@ + + + + + + + + + +Search - slidgram documentation + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+ + + +
+ +
+
+ +
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/slidgram/main/user/features.html b/docs/slidgram/main/user/features.html new file mode 100644 index 0000000..724a615 --- /dev/null +++ b/docs/slidgram/main/user/features.html @@ -0,0 +1,391 @@ + + + + + + + + + Features - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Features

+

The table below lists the notable features for slidgram. +Refer to xmpp.org for a longer version, +including all XEPs supported by slidge.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
✅

Multi-User Chat

Full support, including creation, administration and moderation
✅

User Avatar

Telegram contacts have avatars, and the user's XMPP avatar is used to set their Telegram avatar.
✅

Chat State Notifications

All chat states have a telegram equivalent, including active/inactive
✅

Message Delivery Receipts

No real recipient-generated delivery receipts in Telegram, but slidge will emit some when Telegram servers acknowledge a message
✅

Last User Interaction in Presence

Supported
✅

Last Message Correction

Supported
✅

Displayed Markers

Only displayed markers have an equivalence in Telegram. They are also used to sync read state from and to Telegram clients. In groups, you only receive displayed markers for message you have sent, not for messages from other participants.
✅

HTTP File Upload

Supported
🤏

Message Styling

Incoming telegram messages formatting are converted to XEP-0393 styling, with linked URLs shown. Incoming XMPP messages are parsed for message styling tags and converted to telegram formatted text. Additionally, double pipes can be used for spoilers, eg ||spoiler||.
✅

Message Retraction

Supported
✅

Message Reactions

Supported
✅

Message Replies

Supported
✅

Moderated Message Retraction

Supported
+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/user/index.html b/docs/slidgram/main/user/index.html new file mode 100644 index 0000000..fde2028 --- /dev/null +++ b/docs/slidgram/main/user/index.html @@ -0,0 +1,342 @@ + + + + + + + + + For users - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+ +
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/user/registration.html b/docs/slidgram/main/user/registration.html new file mode 100644 index 0000000..eca5e5d --- /dev/null +++ b/docs/slidgram/main/user/registration.html @@ -0,0 +1,341 @@ + + + + + + + + + Registration - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Registration

+

To start using a slidgram, you need to register your XMPP account with it. +The instance has a unique XMPP (aka JID) address, which is just a domain name (e.g., telegram.example.org, without any @ sign in it).

+

There are two ways to register, depending on which XMPP/Jabber client you are using:

+
    +
  • If your client supports adhoc commands (such as Movim, Gajim, or Cheogram), +you can use the “Register” command with the slidgram instance’s +address.

  • +
  • If your client does not support adhoc commands (such as Conversations), you +can register by sending a text message that says “register” to the +slidgram instance’s address.

  • +
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/slidgram/main/user/usage.html b/docs/slidgram/main/user/usage.html new file mode 100644 index 0000000..f9b8d33 --- /dev/null +++ b/docs/slidgram/main/user/usage.html @@ -0,0 +1,366 @@ + + + + + + + + + Usage - slidgram documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+
+ +
+ +
+
+
+

Usage

+
+

Note

+

Telegram is OK with alternative clients, so as long as you’re not doing evil stuff, using slidge +to interact with the telegram network is fine.

+
+
+

Roster

+

Contact JIDs are of the form 123456789@slidge-telegram.example.org where 123456789 is a telegram ID. +If you want to find the telegram ID of someone using their phone number, use slidge’s search feature: +Finding legacy contacts.

+
+
+

Presences

+

Your contacts’ puppet JIDs presence statuses will show when they were last seen online, +and their presence statuses will be set to “away” after 5 minutes.

+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/modules/mod_privilege.lua b/modules/mod_privilege.lua new file mode 100644 index 0000000..22d9486 --- /dev/null +++ b/modules/mod_privilege.lua @@ -0,0 +1,685 @@ +-- XEP-0356 (Privileged Entity) +-- Copyright (C) 2015-2022 Jérôme Poisson +-- +-- This module is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- Some parts come from mod_remote_roster (module by Waqas Hussain and Kim Alvefur, see https://code.google.com/p/prosody-modules/) + +-- TODO: manage external (for "roster" presence permission) when the account with the roster is offline + +local jid = require("util.jid") +local set = require("util.set") +local st = require("util.stanza") +local roster_manager = require("core.rostermanager") +local usermanager_user_exists = require "core.usermanager".user_exists +local hosts = prosody.hosts +local full_sessions = prosody.full_sessions + +local priv_session = module:shared("/*/privilege/session") + +if priv_session.connected_cb == nil then + -- set used to have connected event listeners + -- which allows a host to react on events from + -- other hosts + priv_session.connected_cb = set.new() +end +local connected_cb = priv_session.connected_cb + +-- the folowing sets are used to forward presence stanza +-- the folowing sets are used to forward presence stanza +local presence_man_ent = set.new() +local presence_roster = set.new() + +local _ALLOWED_ROSTER = set.new({'none', 'get', 'set', 'both'}) +local _ROSTER_GET_PERM = set.new({'get', 'both'}) +local _ROSTER_SET_PERM = set.new({'set', 'both'}) +local _ALLOWED_MESSAGE = set.new({'none', 'outgoing'}) +local _ALLOWED_PRESENCE = set.new({'none', 'managed_entity', 'roster'}) +local _PRESENCE_MANAGED = set.new({'managed_entity', 'roster'}) +local _TO_CHECK = {roster=_ALLOWED_ROSTER, message=_ALLOWED_MESSAGE, presence=_ALLOWED_PRESENCE} +local _PRIV_ENT_NS = 'urn:xmpp:privilege:2' +local _FORWARDED_NS = 'urn:xmpp:forward:0' +local _MODULE_HOST = module:get_host() + + +module:log("debug", "Loading privileged entity module ") + + +--> Permissions management <-- + +local config_priv = module:get_option("privileged_entities", {}) + +local function get_session_privileges(session, host) + if not session.privileges then return nil end + return session.privileges[host] +end + + +local function advertise_perm(session, to_jid, perms) + -- send stanza to advertise permissions + -- as expained in § 4.2 + local message = st.message({from=module.host, to=to_jid}) + :tag("privilege", {xmlns=_PRIV_ENT_NS}) + + for _, perm in pairs({'roster', 'message', 'presence'}) do + if perms[perm] then + message:tag("perm", {access=perm, type=perms[perm]}):up() + end + end + local iq_perm = perms["iq"] + if iq_perm ~= nil then + local perm_el = st.stanza("perm", {access="iq"}) + for namespace, ns_perm in pairs(iq_perm) do + local perm_type + if ns_perm.set and ns_perm.get then + perm_type = "both" + elseif ns_perm.set then + perm_type = "set" + elseif ns_perm.get then + perm_type = "get" + else + perm_type = nil + end + perm_el:tag("namespace", {ns=namespace, type=perm_type}):up() + end + message:add_child(perm_el) + end + session.send(message) +end + +local function set_presence_perm_set(to_jid, perms) + -- fill the presence sets according to perms + if _PRESENCE_MANAGED:contains(perms.presence) then + presence_man_ent:add(to_jid) + end + if perms.presence == 'roster' then + presence_roster:add(to_jid) + end +end + +local function advertise_presences(session, to_jid, perms) + -- send presence status for already connected entities + -- as explained in § 7.1 + -- people in roster are probed only for active sessions + -- TODO: manage roster load for inactive sessions + if not perms.presence then return; end + local to_probe = {} + for _, user_session in pairs(full_sessions) do + if user_session.presence and _PRESENCE_MANAGED:contains(perms.presence) then + local presence = st.clone(user_session.presence) + presence.attr.to = to_jid + module:log("debug", "sending current presence for "..tostring(user_session.full_jid)) + session.send(presence) + end + if perms.presence == "roster" then + -- we reset the cache to avoid to miss a presence that just changed + priv_session.last_presence = nil + + if user_session.roster then + local bare_jid = jid.bare(user_session.full_jid) + for entity, item in pairs(user_session.roster) do + if entity~=false and entity~="pending" and (item.subscription=="both" or item.subscription=="to") then + local _, host = jid.split(entity) + if not hosts[host] then -- we don't probe jid from hosts we manage + -- using a table with entity as key avoid probing several time the same one + to_probe[entity] = bare_jid + end + end + end + end + end + end + + -- now we probe peoples for "roster" presence permission + for probe_to, probe_from in pairs(to_probe) do + module:log("debug", "probing presence for %s (on behalf of %s)", tostring(probe_to), tostring(probe_from)) + local probe = st.presence({from=probe_from, to=probe_to, type="probe"}) + prosody.core_route_stanza(nil, probe) + end +end + + +local function on_auth(event) + -- Check if entity is privileged according to configuration, + -- and set session.privileges accordingly + + local session = event.session + local bare_jid = jid.join(session.username, session.host) + if not session.privileges then + session.privileges = {} + end + + local conf_ent_priv = config_priv[bare_jid] + local ent_priv = {} + if conf_ent_priv ~= nil then + module:log("debug", "Entity is privileged") + for perm_type, allowed_values in pairs(_TO_CHECK) do + local value = conf_ent_priv[perm_type] + if value ~= nil then + if not allowed_values:contains(value) then + module:log('warn', 'Invalid value for '..perm_type..' privilege: ['..value..']') + module:log('warn', 'Setting '..perm_type..' privilege to none') + ent_priv[perm_type] = nil + elseif value == 'none' then + ent_priv[perm_type] = nil + else + ent_priv[perm_type] = value + end + else + ent_priv[perm_type] = nil + end + end + -- extra checks for presence permission + if ent_priv.presence == 'roster' and not _ROSTER_GET_PERM:contains(ent_priv.roster) then + module:log("warn", "Can't allow roster presence privilege without roster \"get\" privilege") + module:log("warn", "Setting presence permission to none") + ent_priv.presence = nil + end + -- iq permission + local iq_perm_config = conf_ent_priv["iq"] + if iq_perm_config ~= nil then + local iq_perm = {} + ent_priv["iq"] = iq_perm + for ns, ns_perm_config in pairs(iq_perm_config) do + iq_perm[ns] = { + ["get"] = ns_perm_config == "get" or ns_perm_config == "both", + ["set"] = ns_perm_config == "set" or ns_perm_config == "both" + } + end + else + ent_priv["iq"] = nil + end + + if session.type == "component" then + -- we send the message stanza only for component + -- it will be sent at first for other entities + advertise_perm(session, bare_jid, ent_priv) + set_presence_perm_set(bare_jid, ent_priv) + advertise_presences(session, bare_jid, ent_priv) + end + end + + session.privileges[_MODULE_HOST] = ent_priv +end + +local function on_presence(event) + -- Permission are already checked at this point, + -- we only advertise them to the entity + local session = event.origin + local session_privileges = get_session_privileges(session, _MODULE_HOST) + if session_privileges then + advertise_perm(session, session.full_jid, session_privileges) + set_presence_perm_set(session.full_jid, session_privileges) + advertise_presences(session, session.full_jid, session_privileges) + end +end + +local function on_component_auth(event) + -- react to component-authenticated event from this host + -- and call the on_auth methods from all other hosts + -- needed for the component to get delegations advertising + for callback in connected_cb:items() do + callback(event) + end +end + +if module:get_host_type() ~= "component" then + connected_cb:add(on_auth) +end +module:hook('authentication-success', on_auth) +module:hook('component-authenticated', on_component_auth) +module:hook('presence/initial', on_presence) + + +--> roster permission <-- + +-- get +module:hook("iq-get/bare/jabber:iq:roster:query", function(event) + local session, stanza = event.origin, event.stanza + if not stanza.attr.to then + -- we don't want stanzas addressed to /self + return + end + local node, host = jid.split(stanza.attr.to) + local session_privileges = get_session_privileges(session, host) + + if session_privileges and _ROSTER_GET_PERM:contains(session_privileges.roster) then + module:log("debug", "Roster get from allowed privileged entity received") + -- following code is adapted from mod_remote_roster + local roster = roster_manager.load_roster(node, host) + + local reply = st.reply(stanza):query("jabber:iq:roster") + for entity_jid, item in pairs(roster) do + if entity_jid and entity_jid ~= "pending" then + reply:tag("item", { + jid = entity_jid, + subscription = item.subscription, + ask = item.ask, + name = item.name, + }) + for group in pairs(item.groups) do + reply:tag("group"):text(group):up() + end + reply:up(); -- move out from item + end + end + -- end of code adapted from mod_remote_roster + session.send(reply) + else + module:log("warn", "Entity "..tostring(session.full_jid).." try to get roster without permission") + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end + + return true +end) + +-- set +module:hook("iq-set/bare/jabber:iq:roster:query", function(event) + local session, stanza = event.origin, event.stanza + if not stanza.attr.to then + -- we don't want stanzas addressed to /self + return + end + local from_node, from_host = jid.split(stanza.attr.to) + local session_privileges = get_session_privileges(session, from_host) + + if session_privileges and _ROSTER_SET_PERM:contains(session_privileges.roster) then + module:log("debug", "Roster set from allowed privileged entity received") + -- following code is adapted from mod_remote_roster + if not(usermanager_user_exists(from_node, from_host)) then return; end + local roster = roster_manager.load_roster(from_node, from_host) + if not(roster) then return; end + + local query = stanza.tags[1] + for _, item in ipairs(query.tags) do + if item.name == "item" + and item.attr.xmlns == "jabber:iq:roster" and item.attr.jid + -- Protection against overwriting roster.pending, until we move it + and item.attr.jid ~= "pending" then + + local item_jid = jid.prep(item.attr.jid) + local _, host, resource = jid.split(item_jid) + if not resource then + if item_jid ~= stanza.attr.to then -- not self-item_jid + if item.attr.subscription == "remove" then + local r_item = roster[item_jid] + if r_item then + roster[item_jid] = nil + if roster_manager.save_roster(from_node, from_host, roster) then + session.send(st.reply(stanza)) + roster_manager.roster_push(from_node, from_host, item_jid) + else + roster[item_jid] = item + session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")) + end + else + session.send(st.error_reply(stanza, "modify", "item-not-found")) + end + else + local subscription = item.attr.subscription + if subscription ~= "both" and subscription ~= "to" and subscription ~= "from" and subscription ~= "none" then -- TODO error on invalid + subscription = roster[item_jid] and roster[item_jid].subscription or "none" + end + local r_item = {name = item.attr.name, groups = {}} + if r_item.name == "" then r_item.name = nil; end + r_item.subscription = subscription + if subscription ~= "both" and subscription ~= "to" then + r_item.ask = roster[item_jid] and roster[item_jid].ask + end + for _, child in ipairs(item) do + if child.name == "group" then + local text = table.concat(child) + if text and text ~= "" then + r_item.groups[text] = true + end + end + end + local olditem = roster[item_jid] + roster[item_jid] = r_item + if roster_manager.save_roster(from_node, from_host, roster) then -- Ok, send success + session.send(st.reply(stanza)) + -- and push change to all resources + roster_manager.roster_push(from_node, from_host, item_jid) + else -- Adding to roster failed + roster[item_jid] = olditem + session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")) + end + end + else -- Trying to add self to roster + session.send(st.error_reply(stanza, "cancel", "not-allowed")) + end + else -- Invalid JID added to roster + module:log("warn", "resource: %s , host: %s", tostring(resource), tostring(host)) + session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? + end + else -- Roster set didn't include a single item, or its name wasn't 'item' + session.send(st.error_reply(stanza, "modify", "bad-request")) + end + end -- for loop end + -- end of code adapted from mod_remote_roster + else -- The permission is not granted + module:log("warn", "Entity "..tostring(session.full_jid).." try to set roster without permission") + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end + + return true +end) + + +--> message permission <-- + +local function clean_xmlns(node) + -- Recursively remove "jabber:client" attribute from node. + -- In Prosody internal routing, xmlns should not be set. + -- Keeping xmlns would lead to issues like mod_smacks ignoring the outgoing stanza, + -- so we remove all xmlns attributes with a value of "jabber:client" + if node.attr.xmlns == 'jabber:client' then + for childnode in node:childtags() do + clean_xmlns(childnode) + end + node.attr.xmlns = nil + end +end + +module:hook("message/host", function(event) + local session, stanza = event.origin, event.stanza + local privilege_elt = stanza:get_child('privilege', _PRIV_ENT_NS) + if privilege_elt==nil then return; end + local _, to_host = jid.split(stanza.attr.to) + local session_privileges = get_session_privileges(session, to_host) + + if session_privileges and session_privileges.message=="outgoing" then + if #privilege_elt.tags==1 and privilege_elt.tags[1].name == "forwarded" + and privilege_elt.tags[1].attr.xmlns==_FORWARDED_NS then + local message_elt = privilege_elt.tags[1]:get_child('message', 'jabber:client') + if message_elt ~= nil then + local username, from_host, from_resource = jid.split(message_elt.attr.from) + if from_resource == nil and hosts[from_host] then -- we only accept bare jids from one of the server hosts + clean_xmlns(message_elt); -- needed do to proper routing + local session = { + username = username; + host = from_host; + type = "c2s"; + log = module._log; + } + -- at this point everything should be alright, we can send the message + prosody.core_post_stanza(session, message_elt, true) + else -- trying to send a message from a forbidden entity + module:log("warn", "Entity "..tostring(session.full_jid).." try to send a message from "..tostring(message_elt.attr.from)) + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end + else -- incorrect message child + session.send(st.error_reply(stanza, "modify", "bad-request", "invalid forwarded element")) + end + else -- incorrect forwarded child + session.send(st.error_reply(stanza, "modify", "bad-request", "invalid element")) + end + else -- The permission is not granted + module:log("warn", "Entity "..tostring(session.full_jid).." try to send message without permission") + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end + + return true +end) + + +--> presence permission <-- + +local function same_tags(tag1, tag2) + -- check if two tags are equivalent + + if tag1.name ~= tag2.name then return false; end + + if #tag1 ~= #tag2 then return false; end + + for name, value in pairs(tag1.attr) do + if tag2.attr[name] ~= value then return false; end + end + + for i=1,#tag1 do + if type(tag1[i]) == "string" then + if tag1[i] ~= tag2[i] then return false; end + else + if not same_tags(tag1[i], tag2[i]) then return false; end + end + end + + return true +end + +local function same_presences(presence1, presence2) + -- check that 2 stanzas are equivalent (except for "to" attribute) + -- /!\ if the id change but everything else is equivalent, this method return false + -- this behaviour may change in the future + if presence1.attr.from ~= presence2.attr.from or presence1.attr.id ~= presence2.attr.id + or presence1.attr.type ~= presence2.attr.type then + return false + end + + if presence1.attr.id and presence1.attr.id == presence2.attr.id then return true; end + + if #presence1 ~= #presence2 then return false; end + + for i=1,#presence1 do + if type(presence1[i]) == "string" then + if presence1[i] ~= presence2[i] then return false; end + else + if not same_tags(presence1[i], presence2[i]) then return false; end + end + end + + return true +end + +local function forward_presence(presence, to_jid) + local presence_fwd = st.clone(presence) + presence_fwd.attr.to = to_jid + module:log("debug", "presence forwarded to "..to_jid..": "..tostring(presence_fwd)) + module:send(presence_fwd) + -- cache used to avoid to send several times the same stanza + priv_session.last_presence = presence +end + +module:hook("presence/bare", function(event) + if presence_man_ent:empty() and presence_roster:empty() then return; end + + local stanza = event.stanza + if stanza.attr.type == nil or stanza.attr.type == "unavailable" then + if not stanza.attr.to then + for entity in presence_man_ent:items() do + if stanza.attr.from ~= entity then forward_presence(stanza, entity); end + end + else -- directed presence + -- we ignore directed presences from our own host, as we already have them + local _, from_host = jid.split(stanza.attr.from) + if hosts[from_host] then return; end + + -- we don't send several time the same presence, as recommended in §7 #2 + if priv_session.last_presence and same_presences(priv_session.last_presence, stanza) then + return + end + + for entity in presence_roster:items() do + if stanza.attr.from ~= entity then forward_presence(stanza, entity); end + end + end + end +end, 150) + +--> IQ permission <-- + +module:hook("iq/bare/".._PRIV_ENT_NS..":privileged_iq", function(event) + local session, stanza = event.origin, event.stanza + if not stanza.attr.to then + -- we don't want stanzas addressed to /self + return + end + local from_node, from_host, from_resource = jid.split(stanza.attr.to) + + if from_resource ~= nil or not usermanager_user_exists(from_node, from_host) then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + "wrapping stanza recipient must be a bare JID of a local user" + ) + ) + return true + end + + local session_privileges = get_session_privileges(session, from_host) + + if session_privileges == nil then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + "no privilege granted" + ) + ) + return true + end + + local iq_privileges = session_privileges["iq"] + if iq_privileges == nil then + session.send( + session.send(st.error_reply(stanza, "auth", "forbidden", "you are not allowed to send privileged stanzas")) + ) + return true + end + + local privileged_iq = stanza:get_child("privileged_iq", _PRIV_ENT_NS) + + local wrapped_iq = privileged_iq.tags[1] + if wrapped_iq == nil then + session.send( + st.error_reply(stanza, "auth", "forbidden", "missing stanza to send") + ) + return true + end + + if wrapped_iq.attr.xmlns ~= "jabber:client" then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'wrapped must have a xmlns of "jabber:client"' + ) + ) + return true + end + + clean_xmlns(wrapped_iq) + + if #wrapped_iq.tags ~= 1 then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'invalid payload in wrapped ' + ) + ) + return true + end + + local payload = wrapped_iq.tags[1] + + local priv_ns = payload.attr.xmlns + if priv_ns == nil then + session.send( + st.error_reply(stanza, "auth", "forbidden", "xmlns not set in privileged ") + ) + return true + end + + local ns_perms = iq_privileges[priv_ns] + local iq_type = stanza.attr.type + if ns_perms == nil or iq_type == nil or not ns_perms[iq_type] then + session.send( + session.send(st.error_reply( + stanza, + "auth", + "forbidden", + "you are not allowed to send privileged stanzas of this type and namespace") + ) + ) + return true + end + + if wrapped_iq.attr.from ~= nil and wrapped_iq.attr.from ~= stanza.attr.to then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'wrapped "from" attribute is inconsistent with main "to" attribute' + ) + ) + return true + end + + wrapped_iq.attr.from = stanza.attr.to + + + if wrapped_iq.attr.type ~= iq_type then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'invalid wrapped : type mismatch' + ) + ) + return true + end + + if wrapped_iq.attr.id == nil then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'invalid wrapped : missing "id" attribute' + ) + ) + return true + end + + -- at this point, wrapped_iq is considered valid, and privileged entity is allowed to send it + local username, from_host, _ = jid.split(wrapped_iq.attr.from) + local newsession = { + username = username; + host = from_host; + full_jid = stanza.attr.to; + type = "c2s"; + log = module._log; + } + + module:send_iq(wrapped_iq,newsession) + :next(function (response) + local reply = st.reply(stanza); + response.stanza.attr.xmlns = 'jabber:client' + reply:tag("privilege", {xmlns = _PRIV_ENT_NS}) + :tag("forwarded", {xmlns = _FORWARDED_NS}) + :add_child(response.stanza) + reply.attr.type = response.stanza.attr.type; + session.send(reply) + end, + function(response) + module:log("error", "Error while sending privileged : %s", response); + session.send( + st.error_reply( + stanza, + "cancel", + "internal-server-error" + ) + ) + end) + + return true +end) diff --git a/slidgram/Dockerfile b/slidgram/Dockerfile new file mode 100644 index 0000000..e642ef1 --- /dev/null +++ b/slidgram/Dockerfile @@ -0,0 +1,11 @@ +# Кастомный образ slidgram с поддержкой SOCKS5-прокси для маршрутизации Telegram +# через VPS01 (SSH-туннель SOCKS5 172.27.0.1:1080, вне РФ). +# Базовый: codeberg.org/slidge/slidgram:latest +# Примечание: pysocks (модуль `socks`) УЖЕ есть в базовом образе (PySocks-1.7.1). + +FROM codeberg.org/slidge/slidgram:latest + +# Патч: добавить поддержку прокси в slidgram +COPY patch-telegram.py /tmp/patch-telegram.py +COPY patch-gateway.py /tmp/patch-gateway.py +RUN python /tmp/patch-telegram.py && python /tmp/patch-gateway.py \ No newline at end of file diff --git a/slidgram/patch-gateway.py b/slidgram/patch-gateway.py new file mode 100644 index 0000000..952026c --- /dev/null +++ b/slidgram/patch-gateway.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Патч slidgram/gateway.py: передать SOCKS5-прокси из env SLIDGRAM_PROXY в Client при регистрации.""" +from pathlib import Path + +p = Path("/venv/lib/python3.13/site-packages/slidgram/gateway.py") +src = p.read_text() + +old = ''' tg_client = Client( + str(user_jid.bare), + phone_number=phone, + api_id=registration_form.get("api_id") or config.API_ID, + api_hash=registration_form.get("api_hash") or config.API_HASH, + workdir=global_config.HOME_DIR, + ) +''' +new = ''' _proxy = None + _proxy_url = __import__("os").environ.get("SLIDGRAM_PROXY", "") + if _proxy_url and _proxy_url.startswith("socks5://"): + _h = _proxy_url.split("://", 1)[1] + _host, _, _port = _h.partition(":") + _proxy = dict(scheme="socks5", hostname=_host, port=int(_port)) + tg_client = Client( + str(user_jid.bare), + phone_number=phone, + api_id=registration_form.get("api_id") or config.API_ID, + api_hash=registration_form.get("api_hash") or config.API_HASH, + workdir=global_config.HOME_DIR, + proxy=_proxy, + ) +''' +assert old in src, "gateway.py: шаблон не найден" +p.write_text(src.replace(old, new, 1)) +print("gateway.py patched OK") \ No newline at end of file diff --git a/slidgram/patch-telegram.py b/slidgram/patch-telegram.py new file mode 100644 index 0000000..bc324fd --- /dev/null +++ b/slidgram/patch-telegram.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Патч slidgram/telegram.py: прокинуть SOCKS5-прокси из env SLIDGRAM_PROXY в Client.""" +import re +from pathlib import Path + +p = Path("/venv/lib/python3.13/site-packages/slidgram/telegram.py") +src = p.read_text() + +old = ''' def __init__(self, name: str) -> None: + super().__init__(name, workdir=str(global_config.HOME_DIR)) +''' +new = ''' def __init__(self, name: str) -> None: + import os + _proxy = None + _proxy_url = os.environ.get("SLIDGRAM_PROXY", "") + if _proxy_url and _proxy_url.startswith("socks5://"): + _h = _proxy_url.split("://", 1)[1] + _host, _, _port = _h.partition(":") + _proxy = dict(scheme="socks5", hostname=_host, port=int(_port)) + super().__init__(name, workdir=str(global_config.HOME_DIR), proxy=_proxy) +''' +assert old in src, "telegram.py: шаблон не найден" +p.write_text(src.replace(old, new, 1)) +print("telegram.py patched OK") \ No newline at end of file