Установка Matrix Synapse + Element + Telegram bridge

Установка Matrix Synapse + Element + Telegram bridge (mautrix-telegram) — Вообще серверов Matrix и клиентов достаточно много, мною было сделано вот так например. Не думаю, что это прям мануал, ну некое рабочее решение скажем так я думаю. Поехали, че.

1. Общая схема

Element (браузер)
→ reverse VPS (nginx)
→ основной сервер (nginx)
→ Synapse (127.0.0.1:8008)

Telegram bridge (mautrix-telegram)
→ docker
→ отдельный postgres
→ цепляется к Synapse как appservice
  • Synapse наружу не открыт
  • весь доступ через nginx
  • Element на отдельном домене
  • Telegram bridge отдельно

2. Matrix Synapse

Установка:

apt update
apt install -y matrix-synapse-py3 postgresql

Файл: /etc/matrix-synapse/homeserver.yaml

server_name: "matrix.example.com"   # основной домен Matrix-сервера; именно он будет частью user ID и federation
                                      # пример: @user:matrix.example.com

public_baseurl: "https://matrix.example.com"   # публичный адрес сервера для клиентов; должен совпадать с тем, куда реально ходит клиент

pid_file: "/var/run/matrix-synapse.pid"   # файл PID процесса Synapse; нужен systemd и служебным утилитам

listeners:   # список listener'ов, которые поднимает Synapse
  - port: 8008   # локальный HTTP-порт Synapse
    tls: false   # TLS выключен, потому что шифрование делает nginx
    type: http   # тип listener'а — HTTP
    x_forwarded: true   # доверять X-Forwarded-* заголовкам от reverse proxy; обязательно при nginx перед Synapse
    bind_addresses:   # адреса, на которых будет слушать Synapse
      - '127.0.0.1'   # слушаем только локалхост, наружу Synapse не открыт
    resources:   # какие API обслуживать на этом listener'е
      - names:
          - client   # клиентский API Matrix
          - federation   # federation API для общения с другими серверами
        compress: false   # встроенное сжатие тут выключено; обычно это ок, nginx и так справится

database:   # блок настроек БД
  name: psycopg2   # драйвер PostgreSQL для Python
  args:   # параметры подключения к PostgreSQL
    user: synapse   # пользователь БД
    password: YOUR_SYNAPSE_DB_PASSWORD   # пароль пользователя БД
    database: synapse   # имя базы данных
    host: 127.0.0.1   # PostgreSQL находится локально
    port: 5432   # стандартный порт PostgreSQL
    cp_min: 5   # минимальное число соединений в пуле
    cp_max: 10   # максимальное число соединений в пуле

log_config: "/etc/matrix-synapse/log.yaml"   # путь до конфига логирования Synapse

media_store_path: /var/lib/matrix-synapse/media   # каталог, где хранятся все медиафайлы

signing_key_path: "/etc/matrix-synapse/homeserver.signing.key"   # ключ подписи сервера Matrix; критически важный файл, терять нельзя

trusted_key_servers:   # доверенные key server'ы для federation
  - server_name: "matrix.org"   # брать публичные ключи federation у matrix.org

macaroon_secret_key: "YOUR_MACAROON_SECRET"   # секрет для токенов и ряда внутренних auth-механизмов Synapse

enable_registration: false   # отключает открытую регистрацию новых пользователей через клиент

registration_shared_secret: "YOUR_REGISTRATION_SHARED_SECRET"   # секрет для register_new_matrix_user и похожих способов админской регистрации

app_service_config_files:   # список appservice-конфигов, которые подключаются к Synapse
  - /etc/matrix-synapse/mautrix-telegram.yaml   # registration-файл Telegram bridge

retention:   # политика хранения сообщений
  enabled: true   # включает retention вообще
  default_policy:   # политика по умолчанию
    min_lifetime: 1d   # минимальный срок хранения сообщений
    max_lifetime: 180d   # максимальный срок хранения сообщений
  allowed_lifetime_min: 1d   # самый маленький разрешённый срок, который вообще можно указывать
  allowed_lifetime_max: 1y   # самый большой разрешённый срок

media_retention:   # политика хранения медиафайлов
  local_media_lifetime: 180d   # сколько хранить локально загруженные файлы
  remote_media_lifetime: 30d   # сколько хранить медиа, пришедшие с других серверов

Что тут важно:

  • Synapse слушает только 127.0.0.1
  • TLS не в Synapse, а на nginx
  • x_forwarded: true обязателен
  • SQLite даже не рассматривал, сразу PostgreSQL
  • retention лучше включать сразу, иначе потом всё разрастается

3. nginx на машине с Synapse

Файл: nginx-конфиг для matrix.example.com на основном серваке

server {   # HTTP-виртуалхост для matrix.example.com
    listen 80;   # слушаем обычный HTTP
    server_name matrix.example.com;   # домен этого virtual host

    location = /.well-known/matrix/client {   # точный путь для client well-known
        default_type application/json;   # отдаём JSON
        add_header Access-Control-Allow-Origin *;   # разрешаем запросы с любых origin, нужно клиентам
        return 200 '{"m.homeserver":{"base_url":"https://matrix.example.com"}}';   # говорим клиенту, где его homeserver
    }

    location = /.well-known/matrix/server {   # точный путь для server well-known
        default_type application/json;   # отдаём JSON
        add_header Access-Control-Allow-Origin *;   # CORS-заголовок
        return 200 '{"m.server":"matrix.example.com:443"}';   # говорим federation, куда ходить
    }

    location / {   # всё остальное на HTTP
        return 301 https://$host$request_uri;   # редиректим на HTTPS
    }
}

server {   # HTTPS-виртуалхост для matrix.example.com
    listen 443 ssl http2;   # HTTPS + HTTP/2
    server_name matrix.example.com;   # домен этого virtual host

    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;   # сертификат
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;   # приватный ключ сертификата

    ssl_protocols TLSv1.2 TLSv1.3;   # разрешённые версии TLS

    location = /.well-known/matrix/client {   # client well-known и по HTTPS тоже
        default_type application/json;   # тип ответа
        add_header Access-Control-Allow-Origin * always;   # CORS даже если ответ нестандартный
        return 200 '{"m.homeserver":{"base_url":"https://matrix.example.com"}}';   # база для Matrix-клиентов
    }

    location = /.well-known/matrix/server {   # server well-known и по HTTPS тоже
        default_type application/json;   # тип ответа
        add_header Access-Control-Allow-Origin * always;   # CORS
        return 200 '{"m.server":"matrix.example.com:443"}';   # federation адрес сервера
    }

    location /_matrix {   # весь Matrix API прокидываем в Synapse
        proxy_pass http://127.0.0.1:8008;   # отправляем в локальный Synapse
        proxy_http_version 1.1;   # используем HTTP/1.1 для proxy

        proxy_set_header Host $host;   # передаём оригинальный Host
        proxy_set_header X-Real-IP $remote_addr;   # реальный IP клиента
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;   # цепочка IP через proxy
        proxy_set_header X-Forwarded-Proto https;   # говорим бэкенду, что снаружи был HTTPS

        client_max_body_size 100M;   # лимит размера загружаемых файлов

        proxy_read_timeout 600s;   # сколько ждать ответ от Synapse
        proxy_send_timeout 600s;   # сколько ждать передачу в сторону Synapse
        send_timeout 600s;   # таймаут отправки клиенту

        proxy_buffering off;   # отключаем буферизацию, полезно для real-time API
        proxy_request_buffering off;   # не буферить тело запроса
    }

    location /_synapse/client {   # служебный client API Synapse тоже прокидываем
        proxy_pass http://127.0.0.1:8008;   # на локальный Synapse
        proxy_http_version 1.1;   # HTTP/1.1

        proxy_set_header Host $host;   # оригинальный host
        proxy_set_header X-Real-IP $remote_addr;   # IP клиента
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;   # XFF цепочка
        proxy_set_header X-Forwarded-Proto https;   # внешний протокол HTTPS

        client_max_body_size 100M;   # лимит на размер файла/запроса

        proxy_read_timeout 600s;   # таймаут чтения
        proxy_send_timeout 600s;   # таймаут отправки в upstream
        send_timeout 600s;   # таймаут для клиента

        proxy_buffering off;   # без буферизации
        proxy_request_buffering off;   # без буферизации запроса
    }

    location / {   # всё остальное на этом домене
        return 404;   # сразу режем
    }
}

Тут главное  — наружу отдаётся только то, что реально нужно Matrix. Всё остальное режется в 404.

4. Reverse nginx на VPS для Matrix

Файл: nginx-конфиг на внешнем reverse VPS

server {   # HTTP на внешнем reverse VPS
    listen 80;   # IPv4 HTTP
    listen [::]:80;   # IPv6 HTTP
    server_name matrix.example.com;   # домен Matrix

    location /.well-known/matrix/server {   # server well-known
        default_type application/json;   # ответ JSON
        add_header Access-Control-Allow-Origin *;   # CORS
        return 200 '{"m.server":"matrix.example.com:443"}';   # указываем адрес federation
    }

    location /.well-known/matrix/client {   # client well-known
        default_type application/json;   # JSON
        add_header Access-Control-Allow-Origin *;   # CORS
        return 200 '{"m.homeserver":{"base_url":"https://matrix.example.com"}}';   # адрес homeserver для клиентов
    }

    location / {   # всё остальное на 80
        return 301 https://$host$request_uri;   # редирект на HTTPS
    }
}

server {   # HTTPS reverse proxy на VPS
    listen 443 ssl http2;   # IPv4 HTTPS + HTTP/2
    listen [::]:443 ssl http2;   # IPv6 HTTPS + HTTP/2
    server_name matrix.example.com;   # домен

    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;   # сертификат VPS
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;   # ключ VPS

    ssl_protocols TLSv1.2 TLSv1.3;   # разрешённые версии TLS
    ssl_prefer_server_ciphers off;   # не навязывать серверный выбор шифров

    location /.well-known/matrix/server {   # server well-known по HTTPS
        default_type application/json;   # JSON
        add_header Access-Control-Allow-Origin *;   # CORS
        return 200 '{"m.server":"matrix.example.com:443"}';   # federation адрес
    }

    location /.well-known/matrix/client {   # client well-known по HTTPS
        default_type application/json;   # JSON
        add_header Access-Control-Allow-Origin *;   # CORS
        return 200 '{"m.homeserver":{"base_url":"https://matrix.example.com"}}';   # homeserver URL
    }

    location ~ ^(/_matrix|/_synapse/client) {   # проксируем только Matrix API и нужный Synapse API
        proxy_pass https://YOUR_PUBLIC_IP;   # прокидываем на основной сервер по HTTPS
        proxy_http_version 1.1;   # HTTP/1.1 до upstream

        proxy_set_header Host $host;   # сохраняем оригинальный host
        proxy_set_header X-Real-IP $remote_addr;   # IP клиента
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;   # цепочка proxy
        proxy_set_header X-Forwarded-Proto https;   # говорим, что внешний протокол HTTPS

        proxy_ssl_server_name on;   # включить SNI при TLS к upstream
        proxy_ssl_verify off;   # не проверять сертификат домашнего сервера; удобно, но менее строго по безопасности

        client_max_body_size 100M;   # лимит размера запроса/файла
        proxy_read_timeout 600s;   # таймаут чтения upstream
        proxy_send_timeout 600s;   # таймаут отправки в upstream
        send_timeout 600s;   # таймаут клиенту

        proxy_buffering off;   # без буферизации ответа
        proxy_request_buffering off;   # без буферизации тела запроса
    }

    location / {   # всё остальное на этом домене
        return 404;   # режем
    }
}

Тут как раз был один из неприятных моментов. Без .well-known и без нормального reverse клиенты особенно на мобилках  хрен авторизовывались. Через такую схему уже всё заработало нормально.

5. Element

Element на отдельном домене.

Файл: config.json

{
  "default_server_config": { // сервер по умолчанию, к которому будет цепляться Element
    "m.homeserver": { // описание homeserver
      "base_url": "https://matrix.example.com", // URL Matrix homeserver
      "server_name": "matrix.example.com" // имя сервера Matrix
    }
  },
  "disable_custom_urls": true, // запретить пользователю вручную менять homeserver в интерфейсе
  "disable_guests": true, // отключить гостевые входы
  "brand": "example Matrix" // название/бренд интерфейса Element
}

nginx на домашней машине для Element:

server {   # HTTP virtual host для Element
    listen 80;   # обычный HTTP
    server_name element.example.com;   # домен Element

    if ($host != "element.example.com") {   # дополнительная проверка host
        return 444;   # молча рвём соединение если host не тот
    }

    return 301 https://$host$request_uri;   # всё валим на HTTPS
}

server {   # HTTPS virtual host для Element
    listen 443 ssl http2;   # HTTPS + HTTP/2
    server_name element.example.com;   # домен Element

    if ($host != "element.example.com") {   # ещё раз проверяем host
        return 444;   # если не тот host — сразу рвём
    }

    ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem;   # сертификат домена Element
    ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem;   # приватный ключ

    ssl_protocols TLSv1.2 TLSv1.3;   # разрешённые версии TLS
    ssl_prefer_server_ciphers off;   # не форсить шифры сервера

    root /var/www/element;   # каталог, где лежит собранный Element Web
    index index.html;   # стартовый файл

    location / {   # основная раздача интерфейса
        try_files $uri $uri/ /index.html;   # SPA-логика: всё неизвестное отдаём в index.html
    }

    location = /config.json {   # отдельная обработка config.json
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";   # не кешировать конфиг
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {   # статика
        expires 7d;   # кешируем 7 дней
        access_log off;   # без access log для статики
    }
}

reverse nginx на VPS для Element:

server {   # HTTP virtual host на VPS для Element
    listen 80;   # HTTP
    server_name element.example.com;   # домен Element

    if ($host != "element.example.com") {   # проверка host
        return 444;   # рвём если не тот домен
    }

    return 301 https://$host$request_uri;   # редирект на HTTPS
}

server {   # HTTPS reverse proxy для Element
    listen 443 ssl http2;   # HTTPS + HTTP/2
    server_name element.example.com;   # домен Element

    if ($host != "element.example.com") {   # защита от левых host
        return 444;   # рвём соединение
    }

    ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem;   # сертификат
    ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem;   # ключ

    ssl_protocols TLSv1.2 TLSv1.3;   # версии TLS
    ssl_prefer_server_ciphers off;   # не форсить шифры сервера

    location / {   # весь трафик Element
        proxy_pass https://YOUR_PUBLIC_IP;   # проксируем на основной по HTTPS
        proxy_http_version 1.1;   # HTTP/1.1 до upstream

        proxy_set_header Host $host;   # оригинальный host
        proxy_set_header X-Real-IP $remote_addr;   # IP клиента
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;   # цепочка IP
        proxy_set_header X-Forwarded-Proto https;   # внешний протокол HTTPS

        proxy_read_timeout 300;   # таймаут чтения ответа
        proxy_send_timeout 300;   # таймаут отправки в upstream
        send_timeout 300;   # таймаут клиенту

        proxy_buffering off;   # не буферить
        proxy_request_buffering off;   # не буферить тело запроса

        proxy_ssl_server_name on;   # включить SNI к основному серверу
    }
}

Element сам по себе поднимается просто, но если не сделать нормально config.json и reverse, потом будут вопросы в духе “почему тут логинится, а тут нет”.

6. mautrix-telegram

Bridge у меня живёт в docker.

Файл: /opt/mautrix-telegram/docker-compose.yml

services:   # список сервисов docker compose
  postgres:   # контейнер с PostgreSQL для bridge
    image: postgres:15   # образ Postgres 15
    container_name: mautrix-telegram-db   # имя контейнера
    restart: unless-stopped   # автозапуск, кроме случая явной остановки
    environment:   # переменные окружения для инициализации БД
      POSTGRES_USER: mautrix   # пользователь БД
      POSTGRES_PASSWORD: YOUR_MAUTRIX_DB_PASSWORD   # пароль БД
      POSTGRES_DB: mautrix   # имя БД
    volumes:
      - ./postgres:/var/lib/postgresql/data   # сохраняем данные PostgreSQL на хосте

  mautrix-telegram:   # контейнер самого Telegram bridge
    image: dock.mau.dev/mautrix/telegram:latest   # актуальный образ bridge
    container_name: mautrix-telegram   # имя контейнера bridge
    restart: unless-stopped   # перезапуск при сбоях и после reboot
    depends_on:
      - postgres   # сначала должен стартовать Postgres
    ports:
      - "29317:29317"   # проброс порта appservice наружу/на хост
    volumes:
      - ./data:/data   # конфиги, сессии, логи и прочие данные bridge

Запуск:

cd /opt/mautrix-telegram
docker compose up -d

7. Регистрация appservice для Synapse

Файл: /etc/matrix-synapse/mautrix-telegram.yaml

id: telegram   # идентификатор appservice внутри Synapse
url: http://BRIDGE_HOST_IP:29317   # URL, по которому Synapse будет обращаться к bridge
as_token: YOUR_AS_TOKEN   # токен appservice для bridge
hs_token: YOUR_HS_TOKEN   # токен homeserver для проверки запросов от Synapse
sender_localpart: APPSERVICE_SENDER   # localpart системного appservice-юзера
rate_limited: false   # не применять стандартный rate limit к appservice
namespaces:   # какие user ID и namespace принадлежат bridge
    users:
        - regex: ^@telegrambot:matrix\.example\.com$   # бот bridge
          exclusive: true   # этот юзер полностью принадлежит appservice
        - regex: ^@telegram_.*:matrix\.example\.com$   # ghost-аккаунты Telegram
          exclusive: true   # тоже эксклюзивно за bridge
de.sorunome.msc2409.push_ephemeral: true   # поддержка ephemeral push events
receive_ephemeral: true   # принимать ephemeral-события

Этот файл потом указывается в homeserver.yaml через app_service_config_files.  

8. Основной конфиг mautrix-telegram

Файл: /opt/mautrix-telegram/data/config.yaml

network:   # настройки работы с Telegram API
    api_id: YOUR_TELEGRAM_API_ID   # Telegram API ID с my.telegram.org
    api_hash: YOUR_TELEGRAM_API_HASH   # Telegram API hash

    device_info:   # как bridge будет представляться Telegram
        device_model: mautrix-telegram   # имя устройства
        system_version:   # версия системы; пусто — оставить дефолт/авто
        app_version: auto   # версию приложения определить автоматически
        lang_code: en   # язык приложения
        system_lang_code: en   # язык системы

    animated_sticker:   # как конвертировать анимированные стикеры
        target: gif   # итоговый формат
        convert_from_webm: false   # не пытаться конвертировать из webm дополнительно
        args:   # параметры конвертации
            width: 256   # ширина
            height: 256   # высота
            fps: 25   # кадры в секунду

    member_list:   # настройки синка участников
        max_initial_sync: 100   # сколько участников тянуть при первом sync
        sync_broadcast_channels: false   # не синкать broadcast channels как member list
        skip_deleted: true   # пропускать удалённые аккаунты

    ping:   # пинги к Telegram для проверки связи
        interval_seconds: 30   # интервал пингов
        timeout_seconds: 10   # таймаут пинга

    sync:   # общие лимиты синхронизации
        update_limit: 100   # лимит обновлений за проход
        create_limit: 15   # лимит созданий порталов/комнат за проход
        login_sync_limit: 15   # лимит начального sync после логина
        direct_chats: true   # подтягивать личные чаты

    takeout:   # настройки takeout-режима Telegram
        dialog_sync: false   # не использовать takeout для синка диалогов
        forward_backfill: false   # не тянуть историю вперёд
        backward_backfill: false   # не тянуть историю назад

    max_member_count: -1   # без лимита по размеру чата
    contact_avatars: false   # не использовать аватары контактов из адресной книги
    contact_names: false   # не использовать имена контактов из адресной книги
    always_custom_emoji_reaction: false   # не форсить кастомные emoji reactions
    saved_message_avatar: mxc://maunium.net/XhhfHoPejeneOngMyBbtyWDk   # аватар для Saved Messages
    always_tombstone_on_supergroup_migration: false   # не всегда создавать tombstone при миграции супергруппы
    image_as_file_pixels: 16777216   # порог пикселей, после которого изображение можно трактовать как файл
    disable_view_once: false   # не отключать view once контент
    displayname_template: "{{ if .Deleted }}Deleted account {{ .UserID }}{{ else }}{{ .FullName }}{{ end }}"   # шаблон отображаемого имени ghost-юзеров

bridge:   # логика поведения самого bridge
    command_prefix: '!tg'   # префикс команд bridge
    personal_filtering_spaces: true   # использовать personal filtering spaces
    private_chat_portal_meta: true   # тянуть метаданные для приватных чатов
    async_events: false   # асинхронную обработку событий не использовать
    split_portals: false   # не разделять порталы дополнительно
    resend_bridge_info: false   # не пересылать bridge info повторно
    no_bridge_info_state_key: false   # использовать обычный state key для bridge info
    bridge_status_notices: errors   # уведомления статуса показывать только при ошибках
    unknown_error_auto_reconnect: null   # поведение автопереподключения при неизвестных ошибках
    unknown_error_max_auto_reconnects: 10   # максимум попыток автопереподключения

    bridge_matrix_leave: false   # не зеркалить выход Matrix-пользователя в Telegram
    bridge_notices: false   # не слать лишние bridge notice
    tag_only_on_create: true   # теги ставить только при создании комнаты
    only_bridge_tags: [m.favourite, m.lowpriority]   # какие теги bridge может трогать
    mute_only_on_create: true   # mute применять только при создании
    deduplicate_matrix_messages: false   # дедупликация Matrix-сообщений выключена
    cross_room_replies: false   # ответы между комнатами выключены
    revert_failed_state_changes: false   # не откатывать проваленные state-изменения
    kick_matrix_users: true   # bridge может кикать Matrix-пользователей при необходимости
    enable_send_state_requests: false   # отключить send_state requests

    cleanup_on_logout:   # что делать при logout Telegram-аккаунта
        enabled: false   # cleanup полностью выключен
        manual:
            private: nothing   # для private ничего не делать
            relayed: nothing   # для relayed ничего не делать
            shared_no_users: nothing   # shared без юзеров — ничего
            shared_has_users: nothing   # shared с юзерами — ничего
        bad_credentials:
            private: nothing   # при битых кредах private не чистить
            relayed: nothing   # relayed не чистить
            shared_no_users: nothing   # shared без юзеров не чистить
            shared_has_users: nothing   # shared с юзерами не чистить

    relay:   # режим relay bridge
        enabled: false   # relay выключен
        admin_only: true   # даже если включить — только админам
        allow_bridge: true   # bridge в relay-режиме разрешён
        default_relays: []   # релеи по умолчанию отсутствуют
        message_formats:   # шаблоны сообщений relay-режима
            m.text: "<b>{{ .Sender.DisambiguatedName }}</b>: {{ .Message }}"   # формат обычного текста
            m.notice: "<b>{{ .Sender.DisambiguatedName }}</b>: {{ .Message }}"   # формат notice
            m.emote: "* <b>{{ .Sender.DisambiguatedName }}</b> {{ .Message }}"   # формат emote
            m.file: "<b>{{ .Sender.DisambiguatedName }}</b> sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}"   # формат файла
            m.image: "<b>{{ .Sender.DisambiguatedName }}</b> sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}"   # формат картинки
            m.audio: "<b>{{ .Sender.DisambiguatedName }}</b> sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}"   # формат аудио
            m.video: "<b>{{ .Sender.DisambiguatedName }}</b> sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}"   # формат видео
            m.location: "<b>{{ .Sender.DisambiguatedName }}</b> sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}"   # формат локации
        displayname_format: "{{ .DisambiguatedName }}"   # шаблон displayname для relay

    portal_create_filter:   # фильтр создания порталов/комнат
        mode: deny   # режим deny-list
        list: []   # пустой список запретов
        always_deny_from_login: []   # логин не создаёт ничего из отдельного deny-списка

    permissions:   # права bridge
        "*": relay   # всем остальным минимальные relay-права
        "matrix.example.com": user   # всем локальным пользователям права user
        "@admin:matrix.example.com": admin   # этому юзеру права администратора bridge

database:   # база bridge
    type: postgres   # используем PostgreSQL
    uri: postgres://mautrix:YOUR_MAUTRIX_DB_PASSWORD@postgres/mautrix?sslmode=disable   # строка подключения к БД в контейнере postgres
    max_open_conns: 5   # максимум открытых соединений
    max_idle_conns: 1   # максимум idle-соединений
    max_conn_idle_time: null   # время жизни idle-соединения не ограничено отдельно
    max_conn_lifetime: null   # общее время жизни соединения не ограничено отдельно

homeserver:   # параметры Synapse, к которому цепляется bridge
    address: https://matrix.example.com   # адрес homeserver
    domain: matrix.example.com   # домен homeserver
    software: standard   # тип homeserver
    status_endpoint:   # отдельный endpoint статуса не задан
    message_send_checkpoint_endpoint:   # checkpoint endpoint не задан
    async_media: false   # асинхронную загрузку медиа не использовать
    websocket: false   # websocket-соединение к homeserver не использовать
    ping_interval_seconds: 0   # периодические ping к homeserver выключены

appservice:   # как bridge поднимает свой appservice endpoint
    address: http://BRIDGE_HOST_IP:29317   # внешний адрес appservice
    public_address:   # публичный адрес отдельно не задан
    hostname: 0.0.0.0   # слушать на всех интерфейсах контейнера/хоста
    port: 29317   # порт appservice

    id: telegram   # ID appservice
    bot:   # настройки bridge-бота
        username: telegrambot   # localpart бота
        displayname: Telegram bridge bot   # отображаемое имя бота
        avatar: mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX   # аватар бота

    ephemeral_events: true   # поддержка ephemeral-событий
    async_transactions: false   # транзакции обрабатывать синхронно

    as_token: "YOUR_AS_TOKEN"   # appservice token
    hs_token: "YOUR_HS_TOKEN"   # homeserver token

    username_template: telegram_{{.}}   # шаблон имени ghost-аккаунтов Telegram

matrix:   # Matrix-специфичные настройки bridge
    message_status_events: false   # не слать отдельные status events
    delivery_receipts: false   # не синхронизировать delivery receipts
    message_error_notices: true   # показывать notice при ошибках сообщений
    sync_direct_chat_list: true   # синкать список direct chats
    federate_rooms: true   # разрешить федерацию комнат, созданных bridge
    upload_file_threshold: 5242880   # порог, после которого вложения отправляются как файл
    ghost_extra_profile_info: false   # не тянуть дополнительную инфу профиля ghost-юзеров

analytics:   # аналитика bridge
    token: null   # токен аналитики отсутствует
    url: https://api.segment.io/v1/track   # endpoint аналитики
    user_id: null   # user ID аналитики отсутствует

provisioning:   # provisioning API bridge
    shared_secret: YOUR_PROVISIONING_SECRET   # общий секрет provisioning API
    allow_matrix_auth: true   # разрешить аутентификацию через Matrix
    debug_endpoints: false   # debug endpoints выключены
    enable_session_transfers: false   # перенос сессий выключен

public_media:   # публичная раздача медиа bridge
    enabled: false   # выключено
    signing_key: YOUR_PUBLIC_MEDIA_SIGNING_KEY   # ключ подписи public media
    expiry: 0   # срок жизни не используется
    hash_length: 32   # длина хэша
    path_prefix: /_mautrix/publicmedia   # префикс пути
    use_database: false   # БД для public media не использовать

direct_media:   # direct media server
    enabled: false   # выключено
    server_name: discord-media.example.com   # имя direct media сервера
    well_known_response:   # well-known ответ не задан
    media_id_prefix:   # префикс media ID не задан
    allow_proxy: true   # proxy для direct media разрешён
    server_key: YOUR_DIRECT_MEDIA_SERVER_KEY   # ключ сервера direct media

backfill:   # обратная подгрузка истории
    enabled: false   # выключено
    max_initial_messages: 50   # если включить, тянуть максимум 50 при первом заполнении
    max_catchup_messages: 500   # максимум догоняющих сообщений
    unread_hours_threshold: 720   # считать unread в пределах 720 часов
    threads:
        max_initial_messages: 50   # лимит для thread history
    queue:
        enabled: false   # очередь backfill выключена
        batch_size: 100   # размер пачки
        batch_delay: 20   # задержка между пачками
        max_batches: -1   # без лимита по числу пачек
        max_batches_override: {}   # override-параметры отсутствуют

double_puppet:   # режим double puppeting
    servers:
        anotherserver.example.org: https://matrix.anotherserver.example.org   # внешний сервер для double puppet
    allow_discovery: false   # авто-discovery выключен
    secrets:
        example.com: as_token:foobar   # секрет для домена example.com

encryption:   # настройки E2EE в bridge
    allow: false   # вообще не разрешать encryption
    default: false   # не включать по умолчанию
    require: false   # не требовать encryption
    appservice: false   # appservice encryption выключен
    msc4190: false   # поддержка MSC4190 выключена
    msc4392: false   # поддержка MSC4392 выключена
    self_sign: false   # self-signing выключен
    allow_key_sharing: true   # разрешить шаринг ключей
    pickle_key: YOUR_PICKLE_KEY   # ключ для хранения encrypted session state
    delete_keys:
        delete_outbound_on_ack: false   # не удалять outbound ключи по ack
        dont_store_outbound: false   # outbound ключи хранить
        ratchet_on_decrypt: false   # ratchet при decrypt не делать
        delete_fully_used_on_decrypt: false   # полностью использованные ключи не удалять при decrypt
        delete_prev_on_new_session: false   # старые ключи не удалять при новой сессии
        delete_on_device_delete: false   # не удалять ключи при удалении устройства
        periodically_delete_expired: false   # периодическая очистка истёкших ключей выключена
        delete_outdated_inbound: false   # устаревшие inbound ключи не удалять
    verification_levels:
        receive: unverified   # принимать и от непроверенных
        send: unverified   # отправлять непроверенным разрешено
        share: cross-signed-tofu   # ключи шарить при таком уровне доверия
    rotation:
        enable_custom: false   # кастомную ротацию не включать
        milliseconds: 604800000   # базовый интервал ротации
        messages: 100   # или каждые 100 сообщений
        disable_device_change_key_rotation: false   # при смене девайса ротацию не отключать

env_config_prefix: null   # префикс env-переменных для override отсутствует

logging:   # логирование bridge
    min_level: debug   # минимальный уровень логов
    writers:
        - type: stdout   # вывод логов в stdout контейнера
          format: pretty-colored   # красивый цветной формат
        - type: file   # также писать в файл
          format: json   # формат файла — JSON
          filename: ./logs/bridge.log   # путь до файла лога
          max_size: 100   # максимальный размер файла
          max_backups: 10   # хранить до 10 бэкапов логов
          compress: false   # старые логи не сжимать

Да, конфиг большой.

9. Логин и команды bridge

Бот:

@telegrambot:matrix.example.com

Полезные команды:

login
sync chats
search username
start-chat username
!tg join https://t.me/...

После логина bridge подтягивает чаты и дальше уже можно жить через Matrix.

10. Работа через CLI

Админку я не прикручивал, всё делается через CLI и Admin API.

Создать пользователя:

register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml http://127.0.0.1:8008

Список комнат:

curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
http://127.0.0.1:8008/_synapse/admin/v1/rooms?limit=1000 | jq

Участники комнаты:

curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
http://127.0.0.1:8008/_synapse/admin/v1/rooms/ROOM_ID/members | jq

11. Очистка истории и базы

Без чистки всё это добро нормально так разрастается.

Массовая очистка сообщений старше 30 дней:

curl -s -H "Authorization: Bearer YOUR_ADMIN_TOKEN" 'http://127.0.0.1:8008/_synapse/admin/v1/rooms?limit=1000' | jq -r '.rooms[].room_id' | while read -r room; do room_enc=$(printf '%s' "$room" | jq -sRr @uri); echo "=== PURGE $room ==="; curl -s -X POST "http://127.0.0.1:8008/_synapse/admin/v1/purge_history/$room_enc" -H "Authorization: Bearer YOUR_ADMIN_TOKEN" -H "Content-Type: application/json" -d "{\"purge_up_to_ts\":$(date -d '30 days ago' +%s000)}"; echo; sleep 2; done

Эта команда удалит сообщения старше 30 дней.

Удаление локального медиа старше 30 дней:

curl -X POST \
"http://127.0.0.1:8008/_synapse/admin/v1/media/delete?before_ts=$(date -d '30 days ago' +%s000)" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN"

Очистка media cache:

curl -X POST \
"http://127.0.0.1:8008/_synapse/admin/v1/purge_media_cache?before_ts=$(date -d '30 days ago' +%s000)" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN"

Сжать базу:

su - postgres -c "psql -d synapse -c 'VACUUM FULL;'"

Проверить размер базы:

su - postgres -c "psql -d synapse -c \"SELECT pg_size_pretty(pg_database_size('synapse'));\""

Проверить самые жирные таблицы:

su - postgres -c "psql -d synapse -c \"SELECT relname AS table, pg_size_pretty(pg_total_relation_size(relid)) AS size FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 20;\""

12. Пару заметок например

  • Сначала казалось, что хватит просто поднять Synapse и всё. Хуй там. Без нормального nginx и reverse были странности.
  • .well-known — обязательная штука. Пока не сделал нормально, клиенты вели себя через жопу.
  • Нельзя писать заметку обрезками конфигов. Через неделю уже не помнишь, что именно там было реально важно.
  • SQLite для такого сетапа вообще мимо. Только PostgreSQL.
  • Bridge и история очень быстро раздувают базу, если этим не заниматься.

13. Что ставилось по факту и где что лежит

Чтобы потом не вспоминать по кускам, что вообще ставилось и где это всё живёт, вот коротко по установке уже без простыней конфигов.

На сервере с Synapse:

apt update
apt install -y matrix-synapse-py3 postgresql nginx certbot

Если нужен Docker для bridge:

apt update
apt install -y docker.io docker-compose-plugin
systemctl enable docker
systemctl start docker

Что где лежит:

    • /etc/matrix-synapse/homeserver.yaml — основной конфиг Synapse
    • /etc/matrix-synapse/log.yaml — настройки логов Synapse
    • /etc/matrix-synapse/homeserver.signing.key — ключ подписи сервера, его терять нельзя
    • /var/lib/matrix-synapse/media — локальное хранилище медиа
    • /etc/matrix-synapse/mautrix-telegram.yaml — registration-файл bridge для Synapse
    • /etc/nginx/sites-available/ — nginx-конфиги сайтов
    • /etc/nginx/sites-enabled/ — активные nginx-конфиги через symlink
    • /opt/mautrix-telegram/docker-compose.yml — compose-файл bridge
    • /opt/mautrix-telegram/data/config.yaml — основной конфиг mautrix-telegram
    • /opt/mautrix-telegram/data/logs/ — логи bridge, если включена запись в файл
    • /var/lib/postgresql/ — данные PostgreSQL, если это локальная установка из apt
    • /var/lib/docker/ — данные docker, если bridge и postgres крутятся в контейнерах

Важно: registration YAML и основной config.yaml — это не одно и то же.

    • /etc/matrix-synapse/mautrix-telegram.yaml — registration-файл, который читает сам Synapse через app_service_config_files
    • /opt/mautrix-telegram/data/config.yaml — основной конфиг самого bridge, где уже лежат токены, адрес homeserver, appservice и прочие настройки

Element Web:

Если ставить его просто как статику без всяких плясок, то достаточно распаковать сборку в отдельную папку, например:

  • /var/www/element — файлы Element Web
  • /var/www/element/config.json — его конфиг

Что обычно делается после установки:

  • создаётся база и пользователь PostgreSQL под Synapse
  • правится homeserver.yaml
  • подключается nginx перед Synapse
  • делаются сертификаты
  • отдельно поднимается Element на своём домене
  • отдельно поднимается mautrix-telegram через Docker
  • registration-файл bridge подключается в app_service_config_files
  • после правок Synapse и nginx перезапускаются

Полезно помнить:

  • Synapse как сервис обычно живёт как matrix-synapse
  • nginx — обычный systemctl restart nginx
  • bridge в docker проверяется через docker compose ps и docker compose logs -f
  • если что-то не работает, смотреть надо сразу в три места: Synapse, nginx и bridge

14. PostgreSQL: пользователь и база

В статье выше просто указаны параметры БД, но сам момент создания базы — это отдельная история.

Делается это под пользователем postgres:

su - postgres

Создаём пользователя:

createuser synapse

Создаём базу:

createdb -O synapse synapse

И задаём пароль:

psql
ALTER USER synapse WITH PASSWORD 'YOUR_PASSWORD';
\q

После этого эти данные уже прописываются в homeserver.yaml.

15. Токены bridge (as_token / hs_token)

В конфиге bridge и registration-файле есть два токена:

  • as_token
  • hs_token

Их никто тебе не выдаёт — их надо сгенерировать самому.

Самый простой вариант — просто взять случайные строки:

openssl rand -hex 32

Сгенерил два значения — вставил:

  • в /etc/matrix-synapse/mautrix-telegram.yaml
  • в /opt/mautrix-telegram/data/config.yaml

значения должны совпадать в обоих файлах, иначе bridge просто не подключится.

16. Telegram API (api_id / api_hash)

Без этого bridge вообще не работает.

Получается это тут:

https://my.telegram.org

Дальше:

  • заходишь под своим номером
  • идёшь в API development tools
  • создаёшь приложение
  • получаешь api_id и api_hash

И вставляешь их в config.yaml bridge.

Это делается один раз, дальше оно просто работает.

17. Первый логин в bridge

После запуска bridge он сам по себе никуда не подключён.

Нужно написать боту:

@telegrambot:matrix.example.com

И выполнить:

login

Дальше он попросит номер, код и при необходимости пароль (если включён 2FA).

После логина можно:

sync chats

И он подтянет диалоги.

Всё, дальше уже обычная работа через Matrix.

18. systemd и сервис Synapse

Пакет matrix-synapse-py3 уже создаёт systemd-сервис автоматически.

То есть ничего руками писать не надо.

Основные команды:

systemctl status matrix-synapse
systemctl restart matrix-synapse
systemctl enable matrix-synapse

Если Synapse не стартует — смотреть сюда:

journalctl -u matrix-synapse -xe

Обычно там сразу видно, что именно не так (БД, конфиг, токены и т.д.).

19. Сертификаты Let's Encrypt

В конфиге nginx я сразу использовал сертификаты, но не показал, как они делаются.

Самый простой вариант:

certbot --nginx -d matrix.example.com -d element.example.com

Либо через DNS-челлендж, если не хочешь светить сервер напрямую.

Сертификаты после этого лежат тут:

  • /etc/letsencrypt/live/matrix.example.com/
  • /etc/letsencrypt/live/element.example.com/

И nginx уже использует их напрямую.

Итог

Ну вроде все. Если что-то мною было упущено, потом добавлю.