В свете небезызвестных событий в законотворческой области, столкнулся с необходимостью организовать канал для общения внутри семьи, т.к. пользоваться звонками в популярных мессенджерах – значит быть подверженным угрозам со стороны мошенников и спонсировать терроризм, а звонить по мобильной сети с ее ужасным качеством связи (несмотря на все потуги операторов в VoLTE и прочие VoiceHD) в 2025 году – какой-то моветон. А MAX на мои устройства устанавливаться отказался, не знаю почему, я даже не пробовал. Может быть потому что я слишком мало времени провожу в лифте и на парковке?
Еще с 2013 года я заразился NAS’остроением, поэтому проблем с выбором VDS/VPS у меня небыло: домашний «NAS» за это время превратился из WD MyCloud с установленным OpenMediaVault в уже вполне себе самостоятельный мини-сервер, но все с тем же OpenMediaVault в качестве хостовой системы (да-да, Debian с веб-мордой, я пробовал и Proxmox и все остальное – не зашло по тем или иным причинам. Если статья зайдет аудитории Хабра — напишу подробно про все этапы превращения домашней файлопомойки во вполне себе функциональный ящичек).
Одиноко скучающий детёныш сервера
В качестве среды для всего, что мы сегодня будем городить, я предпочел Docker. А разворачивать все это лично мне удобнее через Portainer.
Первым вопросом встал выбор протокола, который будет использоваться для звонков вместо всех этих мошеннических Telegram и террористических WhatsApp.
Судя по форумам, в качестве протоколов самое популярное и доступное для селфхостинга – Matrix и XMPP. Определиться, что же лучше, по описаниям и отзывам я не смог – поэтому решил поставить сразу два сервера, под каждый протокол, и уже по ходу дела выбрать победителя.
В качестве сервера Matrix выбор пал на Synapse, в качестве сервера XMPP — Ejabberd. Возможно, есть что-то лучше, но у этих двоих довольно понятные мануалы и оба без проблем разворачиваются в Docker.
При выборе клиентов я руководствовался по большей части эстетическими соображениями, выбирая на основании юзабельности и симпатичности клиентов, ведь использовать все это будут в том числе родители и супруга, а как известно, женщина – лучший показатель применимости технологии. Если женщина начала чем-то пользоваться – значит оно действительно работает. Больше всего приглянулся Element для Matrix (не X, в Х чтоб поднять звонки нужно было слишком уж заморочаться) – мультиплатформенный, без лишних рюшечек, и Conversation для XMPP, но так как с мультиплатформенностью у него туго — на iOS в качестве клиента выбор пал на Monal.
Element на Android и iOS соответственно


Клиенты XMPP на Android и iOS соответственно


Итого «стек» технологий выглядел примерно так: разворачивать все будем в Docker с помощью Portainer, для неискушенного командной строкой пользователя это вполне доступный метод.
Протокол Matrix у нас будет обеспечиваться сервером Synapse, XMPP – Ejjaberd.
Так же для работы аудиозвонков нам потребуется сервер Coturn, который будет пробиваться за NAT (с ним кстати плясок с бубнами оказалось больше всего).
На сервере у меня установлен Nginx Proxy Manager (далее для краткости – NPM), который разрешает доменные имена третьего уровня и получает сертификаты, через него наши клиенты и будут обращаться к серверной части.
Если представить все в виде блок-схемы, то получим такое:
В этом руководстве я подразумеваю, что у вас уже установлен Docker, Portainer, NPM, имеется доменное имя и вы +/- представляете, что с этим всем делать. Я не претендую на идеальную правильность приведенных ниже решений – я у мамы инженер-физик, а не программист, поэтому с удовольствием прислушаюсь к вашим советам и рекомендациям. Итак, начинаем.
Первым что нам необходимо сделать – зарегистрировать записи в DNS. Представим что ваш личный сервер имеет адрес myserver.ru. Идем к регистратору и создаем следующие записи:
-
А‑запись: synapse.myserver.ru — для обращения к серверу Synapse;
-
А‑запись: element.myserver.ru — для обращения к web‑клиенту Element;
-
А‑запись: xmpp.myserver.ru — для обращения к Ejabberd, через нее же будем подключаться к Coturn и возьмем для него сертификаты;
-
SRV‑запись: xmpp‑client.tcp.myserver.ru с указанием target xmpp.myserver.ru и порта 5222 — для подключения клиентов xmpp;
-
SRV‑запись: xmpp‑server.tcp.myserver.ru с указанием target xmpp.myserver.ru и порта 5269 — для подключения федерации серверов xmpp.
Вторым – создадим папки на сервере, где будут храниться конфиги и прочие потроха сервисов. Для упрощения будем считать что вы выделили под все это папку в разделе /opt и назвали ее /myservice.
Создаем следующую структуру папок:

mkdir -p /opt/myservice/ejabberd/{certs,conf,data,logs,upload} /opt/matrix/{data,element,pg}
Теперь создадим конфиги под наши будущие сервисы:
Сервер Ejabberd:
делаем в терминале
nano /opt/myservice/ejabberd/conf/ejabberd.yml
и вставляем туда следующий конфиг:
ejabberd.yml
loglevel: 4 # Уровень логирования (0 - минимум, 5 - максимум). 4 = подробные логи log_rotate_size: 10485760 # Максимальный размер файла лога (10 МБ) log_rotate_count: 1 # Количество сохраняемых старых файлов логов (1 — только один архив) hosts: # Список доменов, обслуживаемых сервером XMPP - "xmpp.myserver.ru" # Основной XMPP-домен сервера acl: # Access Control Lists — списки доступа admin: # Группа администраторов user: - "admin@xmpp.myserver.ru" # Пользователь с правами администратора acme: auto: false # Автоматическое получение SSL-сертификатов через Let's Encrypt выключено access_rules: # Правила доступа для различных сервисов local: # Локальные подключения (например, скрипты) allow: all # Разрешены всем c2s: # Client-to-Server (подключение клиентов) allow: all # Разрешено всем пользователям s2s: # Server-to-Server (федерация XMPP) allow: all # Разрешено соединение с любыми серверами configure: # Доступ к административным функциям allow: admin # Разрешено только администраторам listen: # Конфигурация прослушиваемых портов - port: 5222 # TCP-порт для клиентских подключений (c2s) module: ejabberd_c2s # Модуль для Client-to-Server соединений starttls: true # Поддержка STARTTLS starttls_required: true# Обязательно использовать TLS max_stanza_size: 65536 # Максимальный размер XMPP-сообщения (в байтах) shaper: c2s_shaper # Ограничение скорости для клиентов - port: 5269 # TCP-порт для серверных соединений (s2s) module: ejabberd_s2s_in# Модуль для Server-to-Server соединений max_stanza_size: 131072# Максимальный размер пакета (128 КБ) shaper: s2s_shaper # Ограничение скорости для серверов - port: 5280 # HTTP-интерфейс module: ejabberd_http # Модуль веб-доступа request_handlers: # Обработчики путей "/admin": ejabberd_web_admin # Веб-интерфейс админики "/api": mod_http_api # HTTP API "/bosh": mod_bosh # BOSH (HTTP-подключение XMPP-клиентов) "/ws": ejabberd_http_ws # WebSocket-подключения "/upload": mod_http_upload # HTTP File Upload tls: false # HTTPS не используется (работает только через HTTP) - port: 3478 # UDP-порт для STUN/TURN (VoIP, WebRTC) transport: udp module: ejabberd_stun # Модуль STUN/TURN use_turn: true # Включен TURN (ретрансляция медиа-трафика) turn_min_port: 49152 # Минимальный порт для медиапотоков turn_max_port: 65535 # Максимальный порт для медиапотоков - port: 5349 # TCP-порт для STUN/TURN с TLS transport: tcp module: ejabberd_stun use_turn: true tls: true # Поддержка шифрования TLS turn_min_port: 49152 turn_max_port: 65535 certfiles: # Пути к SSL-сертификатам - "/etc/letsencrypt/fullchain.pem" # Полная цепочка сертификатов - "/etc/letsencrypt/privkey.pem" # Приватный ключ default_db: internal # Используется встроенная база данных ejabberd (Mnesia) modules: # Подключенные модули ejabberd mod_adhoc: {} # Ad-hoc команды (XEP-0050) mod_admin_extra: {} # Дополнительные административные функции mod_announce: # Модуль объявлений (broadcast) access: admin # Только администраторы могут рассылать mod_avatar: {} # Поддержка аватаров пользователей mod_blocking: {} # Блокировка контактов (XEP-0191) mod_bosh: {} # Поддержка BOSH mod_caps: {} # Entity Capabilities (XEP-0115) mod_carboncopy: {} # Сообщения копируются на все устройства (XEP-0280) mod_client_state: {} # Состояние клиента (idle/active) mod_configure: {} # Конфигурация через XMPP mod_disco: {} # Service Discovery (XEP-0030) mod_http_api: {} # REST API mod_http_upload: # HTTP File Upload (XEP-0363) put_url: "https://xmpp.myserver.ru/upload" # URL для загрузки файлов get_url: "https://xmpp.myserver.ru/upload" # URL для скачивания файлов docroot: "/home/ejabberd/upload" # Папка для хранения файлов max_size: 104857600 # Максимальный размер файла (100 МБ) mod_last: {} # Последняя активность пользователя (XEP-0012) mod_mam: # Message Archive Management (XEP-0313) default: always # Всегда сохранять сообщения mod_muc: # Multi-User Chat (XEP-0045) access: all # Доступен всем access_create: all # Любой может создавать комнаты access_persistent: all # Разрешены постоянные комнаты access_admin: admin # Администрирование комнат — только админ mod_ping: {} # Ping (XEP-0199) mod_privacy: {} # Privacy Lists (XEP-0016) mod_private: {} # Хранение приватных данных (XEP-0049) mod_pubsub: # PubSub (XEP-0060) access_createnode: all # Разрешено всем создавать узлы plugins: # Подключенные плагины - "flat" # Простая структура - "pep" # Personal Eventing Protocol (XEP-0163) force_node_config: # Настройки по умолчанию для узлов "urn:xmpp:microblog:0": # Микроблоги deliver_payloads: true # Доставлять данные сразу notify_retract: true # Уведомлять об удалении persist_items: true # Хранить данные max_items: 100 # Максимум 100 сообщений mod_push: {} # Push-уведомления mod_push_keepalive: {} # Поддержка keepalive для push mod_register: # Регистрация пользователей access: none # Отключена (запрет регистрации) ip_access: none # Нельзя регистрироваться ни с какого IP registration_watchers: # Уведомления о регистрации - "admin@xmpp.myserver.ru" # Администратор получает уведомления
Сохраняем и переходим к конфигу Synapse.
Делаем:
nano /opt/myservice/matrix/data/homeserver.yaml
и вставляем в него следующее:
homeserver.yaml
server_name: "synapse.myserver.ru" # Основное имя сервера Matrix pid_file: /data/homeserver.pid # Файл, где будет храниться PID процесса Synapse # Задаем порты и протоколы для клиентов и федерации listeners: - port: 8008 # Порт, на котором работает Synapse tls: false # TLS выключен type: http # Тип протокола x_forwarded: true # Разрешает доверять заголовку X-Forwarded-For от прокси resources: - names: [client, federation] # Разрешённые ресурсы: клиентские запросы и федерация compress: false # Отключено сжатие (для производительности) # Настройки базы данных (PostgreSQL) database: name: psycopg2 # Используемый драйвер PostgreSQL txn_limit: 10000 # Максимальное число транзакций в одном пуле args: user: synapse # Пользователь БД password: mypassword # Пароль database: synapse # Имя базы данных host: synapse-db # Хост базы данных (контейнер/сервер) port: 5432 # Порт PostgreSQL cp_min: 5 # Минимальное число соединений в пуле cp_max: 10 # Максимальное число соединений в пуле # Настройки логов (раскомментировать при необходимости) #log_config: "log.yaml" # Директория для хранения медиа (файлы, изображения и т.п.) media_store_path: /data/media_store # Убирает предупреждение о key сервере suppress_key_server_warning: true # Максимальный размер загружаемых файлов max_upload_size: 400M # Регистрация новых пользователей запрещена (false) enable_registration: false # Включена федерация с другими Matrix-серверами matrix_synapse_federation_enabled: true matrix_synapse_federation_port_enabled: true # Общий секрет для регистрации пользователей через API registration_shared_secret: "mypassword" # Разрешает поиск по всем пользователям сервера search_all_users: true # Предпочитать локальных пользователей (вместо удалённых из федерации) prefer_local_users: true # Настройки TURN-сервера (для звонков и WebRTC) turn_uris: - "turn:xmpp.myserver.ru:3478?transport=udp" - "turn:xmpp.myserver.ru:3478?transport=tcp" - "turns:xmpp.myserver.ru:5349?transport=udp" - "turns:xmpp.myserver.ru:5349?transport=tcp" turn_shared_secret: "mysecret" # Секретный ключ для генерации учетных данных TURN turn_user_lifetime: 86400000 # Время жизни учётной записи (мс) turn_allow_guests: true # Разрешить гостям использовать TURN turn_server_name: "synapse.myserver.ru" # Имя TURN-сервера # Список администраторов сервера admin_users: - "@admin:synapse.myserver.ru" # Отправлять анонимную статистику разработчикам Synapse report_stats: false # Секретные ключи для подписи токенов macaroon_secret_key: "mnogobukv" form_secret: "mnogobukv" # Ключ для подписи событий (не удалять - сообщения потом не расшифруются) signing_key_path: "/data/synapse.myserver.ru.signing.key" # Экспериментальные функции Matrix (включены новые спецификации) experimental_features: call: true msc3266_enabled: true msc4222_enabled: true msc4140_enabled: true # Максимальная задержка доставки события max_event_delay_duration: 24h # Ограничения скорости отправки сообщений (rate limiting) rc_message: per_second: 0.5 # Средняя скорость (0.5 сообщений в секунду) burst_count: 30 # Максимальный "всплеск" сообщений # Ограничения на управление отложенными событиями rc_delayed_event_mgmt: per_second: 1 burst_count: 20 # Политика хранения медиа (очистка старых файлов) media_retention: local_media_lifetime: 120d # Срок хранения локальных файлов remote_media_lifetime: 120d # Срок хранения удалённых файлов
затем
nano /opt/myservice/matrix/config.json
и вставляем:
config.json
{ "homeserver_url": "https://synapse.myserver.ru", "enable_presence_by_hs_url": { "https://synapse.myserver.ru": true }, "turn": { "urls": [ "turn:xmpp.myserver.ru:3478?transport=udp", "turn:xmpp.myserver.ru:3478?transport=tcp", "turns:xmpp.myserver.ru:5349?transport=udp", "turns:xmpp.myserver.ru:5349?transport=tcp" ], "secret": "mysecret", "expiry": 864000000, "turn_allow_guest": true }, "terms_and_conditions_links": [ { "url": "https://myserver.ru/privacy", "text": "Privacy Policy" }, { "url": "https://myserver.ru/cookie-policy", "text": "Cookie Policy" } ], "privacy_policy_url": "https://myserver.ru/privacy" }
Теперь настроим клиент Element-web:
снова создаем файл
nano /opt/myservice/matrix/element/config.json
и вставляем
config.json
{ "default_server_config": { "m.homeserver": { "base_url": "https://synapse.myserver.ru", "server_name": "synapse.myserver.ru" }, "io.element.call": { "url": "https://call.element.io" }, "io.element.e2ee": { "default": true }, "m.identity_server": { "base_url": "https://vector.im" } }, "disable_custom_urls": true, "disable_guests": true, "disable_login_language_selector": true, "disable_3pid_login": true, "brand": "My Personal Server", "enable_element_call": true, "integrations_ui_url": "https://scalar.vector.im/", "integrations_rest_url": "https://scalar.vector.im/api", "integrations_widgets_urls": [ "https://scalar.vector.im/_matrix/integrations/v1", "https://scalar.vector.im/api", "https://scalar-staging.vector.im/_matrix/integrations/v1", "https://scalar-staging.vector.im/api", "https://scalar-staging.riot.im/scalar/api" ], "bug_report_endpoint_url": "https://element.io/bugreports/submit", "uisi_autorageshake_app": "element-auto-uisi", "default_country_code": "RU", "show_labs_settings": false, "features": {}, "default_federate": false, "default_theme": "light", "room_directory": { "servers": ["synapse.myserver.ru"] }, "element_call": { "url": "https://call.element.io", "participant_limit": 8, "brand": "Element Call" }, "enable_presence_by_hs_url": { "https://synapse.myserver.ru": true }, "terms_and_conditions_links": [ { "url": "https://element.io/privacy", "text": "Privacy Policy" }, { "url": "https://element.io/cookie-policy", "text": "Cookie Policy" } ], "privacy_policy_url": "https://element.io/cookie-policy" }
Не забываем все это сохранять и убедитесь что ваш Docker имеет права на эти папки и файлы!
Идем запускать контейнеры.
Первым пойдет Coturn.
Идем в Portainer, раздел Stacks и нажимаем Add Stack:

Пишем в названии стека coturn, в окно web-editor вставляем следующее:
coturn
services: coturn: image: coturn/coturn # Образ coturn container_name: coturn # Имя контейнера restart: unless-stopped # Автоперезапуск ports: # Пробрасываем порты наружу - "3478:3478/udp" # TURN (UDP) - "3478:3478/tcp" # TURN (TCP) - "5349:5349/tcp" # TLS-over-TCP (TURNs) - "5349:5349/udp" # TLS-over-UDP (TURNs) - "49152-65535:49152-65535/udp" # Диапазон портов для передачи медиаданных (нужно открыть на роутере) volumes: - /opt/myservices/ejabberd/certs:/etc/letsencrypt:ro # Подключаем сертификаты Let's Encrypt (только для чтения) command: > -n # Режим запуска --turn_allow_guests=true # Разрешаем гостевые подключения без учётки --use-auth-secret # Используем динамическую аутентификацию --static-auth-secret=mysecret # Секрет для генерации временных логинов/паролей --rearm=xmpp.myserver.ru # Разрешаем подключение --cert=/etc/letsencrypt/fullchain.pem # Путь к TLS-сертификату --pkey=/etc/letsencrypt/privkey.pem # Путь к приватному ключу
Запускаем все это дело и смотрим в логи. Должен ругнуться на сертификаты, их пока нет, но будут. Останавливаем чтоб не мешал.
Далее поднимаем Ejabberd:
Аналогично как и с Coturn, открываем окно Web-editor, обзываем стек и вставляем туда следующее:
ejabberd
services: ejabberd: image: ejabberd/ecs # Образ ejabberd container_name: ejabberd # Имя контейнера restart: unless-stopped # Автоперезапуск контейнера environment: - ERLANG_NODE=ejabberd@localhost # Имя Erlang-ноды, используемой ejabberd - XMPP_DOMAIN=xmpp.myserver.ru # Домен XMPP сервера (Jabber ID будет вида user@xmpp.myserver.ru) volumes: - /opt/myservice/ejabberd/conf:/home/ejabberd/conf # Конфиги ejabberd - /opt/myservice/ejabberd/data:/home/ejabberd/database # Хранилище данных (базы) - /opt/myservice/ejabberd/logs:/home/ejabberd/logs # Логи сервера - /opt/myservice/ejabberd/upload:/home/ejabberd/upload # Файлы, загружаемые пользователями - /opt/myservice/ejabberd/certs:/etc/letsencrypt:ro # Сертификаты Let's Encrypt для TLS (только чтение) ports: - "5222:5222" # Клиентский порт XMPP (TCP, STARTTLS) - "5269:5269" # Порт федерации XMPP (сервер-сервер) - "5280:5280" # HTTP(S) порты (веб-админка)
Запускаем, останавливаем.
Далее – стек Synapse:
synapse
services: synapse: image: matrixdotorg/synapse:latest # Образ Synapse container_name: matrix-synapse # Имя контейнера restart: unless-stopped # Автоперезапуск контейнера environment: - SYNAPSE_SERVER_NAME=synapse.myserver.ru # Домен сервера - SYNAPSE_REPORT_STATS=false # Отправка анонимной статистики разработчикам volumes: - /opt/myservice/matrix/data:/data # Тут храним конфиги Synapse ports: - 8008:8008 # Порт для клиентов - 8448:8448 # Порт для федерации с другими серверами healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8008/health"] # Проверка что не сдох interval: 30s # Проверять каждые 30 секунд timeout: 10s # Таймаут на выполнение healthcheck retries: 5 # Количество повторных попыток element-web: image: vectorim/element-web:latest # Образ Element Web container_name: element-web restart: unless-stopped volumes: - /opt/myservice/matrix/element/config.json:/app/config.json # Конфиги Element Web ports: - 8009:80 # Внешний порт для доступа к веб-клиенту synapse-db: image: docker.io/postgres:latest # Образ PostgreSQL для Synapse container_name: synapse-db hostname: synapse-db restart: unless-stopped environment: TZ: "Europe/Moscow" # Часовой пояс POSTGRES_USER: synapse # Пользователь БД POSTGRES_PASSWORD: password # Пароль БД POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C # Инициализация базы с правильной локалью volumes: - /opt/myservice/matrix/pg:/var/lib/postgresql/data # Данные БД сохраняются вне контейнера synapse-admin: image: awesometechnologies/synapse-admin:latest # Веб-интерфейс для управления Synapse container_name: synapse-admin restart: unless-stopped ports: - 8007:80 # Внешний порт для доступа к веб-админке environment: - SYNAPSE_ADMIN_API=http://matrix-synapse:8008 # Адрес админки
Запустили, остановили. Идем получать сертификаты.
Открываем NPM и добавляем проксируемые хосты:
Для xmpp.myserver.ru:

Во вкладке SSL не забываем получить сертификаты, их мы будем активно использовать далее.
Важное примечание для xmpp – чтоб клиенты могли обмениваться файлами – ему надо указать куда эти файлы ложить и откуда брать, поэтому в разделе Custom Location делаем так, указывая IP вашего сервера в локалке:
Для synapse.myserver.ru:

Тут тоже есть нюанс, чтоб Element нормально работал – ему нужен .well-known, поэтому в раздел Advanced вставляем следующее:
.well-known
location /.well-known/matrix/server { default_type application/json; add_header Access-Control-Allow-Origin *; return 200 '{"m.server": "synapse.myserver.ru:443"}'; } location /.well-known/matrix/client { default_type application/json; add_header Access-Control-Allow-Origin *; return 200 '{ "m.homeserver": { "base_url": "https://synapse.myserver.ru" }, "io.element.e2ee": { "default": true } }'; } proxy_read_timeout 360s; proxy_buffering off;
Для Element-Web:

Тут тоже надо дописать в Advanced:
location
location /.well-known/matrix/client { add_header Access-Control-Allow-Origin "*"; add_header Content-Type "application/json"; return 200 '{"m.homeserver":{"base_url":"https://synapse.myserver.ru"}}'; }
По итогу всего этого у вас должно получиться примерно так:

Хосты настроили, теперь идем за сертификатами. Которые вы естественно для каждой записи в NPM получили. Не забыли же? По умолчанию NPM сохраняет сертификаты к себе в папку /live/npm-xxx, где ххх – номер проксируемого хоста. Поэтому идем за ними. Смотрим какой номер у сертификата xmpp.myserver.ru:
В нашем случае 142, копируем их к нам в папку, не забываем менять путь к папке с сертификатами на свой! В моем случае папка NPM расположена по пути /portainer/npm/live/:
cp /portainer/npm/live/npm-142/fullchain.pem /opt/myservice/ejabberd/certs/fullchain.pem cp /portainer/npm/live/npm-142/privkey.pem /opt/myservice/ejabberd/certs/privkey.pem
Когда сертификаты скопированы, возвращаемся в Portainer и запускаем наши контейнеры.
Для Ejabberd нужно создать админскую учетку, для этого заходим в консоль и выполняем следующее:
docker exec -it ejabberd ejabberdctl register admin myserver.ru password
Для Synapse мы его уже прописали в homeserver.yaml
После всего проделанного
Админка Ejabberd будет доступна по адресу ip:5280/admin

Админка Synapse будет доступна по адресу ip:8007

Web-клиент Element будет находится по адресу element.myserver.ru

Если нигде не ошиблись – можно скачивать понравившиеся вам клиенты и подключаться.
В итоге, попользовавшись всем этим несколько дней, остановился на Synapse и Element. Хотя визуально Conversation и выглядит просто отлично — ничего лишнего, все работает, но XMPP мне не зашел, подвели устройства Apple, на которых ни один из клиентов не захотел работать в фоне. Сообщения можно было получить только открыв приложение и подождав пока они загрузятся. Плюс не понравилось хранение и передача файлов по сути ссылками на открытые папки. Если кто подскажет путь решения этих проблем — буду благодарен.
Одним из жирных плюсов для себя так же выделил разделение потоков информации — новости, мемчики и котики в Telegram, семья — в Element.
ссылка на оригинал статьи https://habr.com/ru/articles/942758/
Добавить комментарий