Skip to content

ArtemIsmagilov/mm-yc-notify

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mattermost - Yandex Calendar Интеграция

header

Содержание

Задача

📝 В Яндекс Календаре назначаются встречи. Нужно как по ссылке YandexCalendar(plugin)

  • Присылать утром список встреч на день.
  • Информировать за 10 минут до встречи, показывать ссылку на яндекс-телемост у встречи.
  • Сообщать об отменённых встречах, переносах встреч, добавленных встречах.
  • Сообщать о действиях участников встреч со мной (не подтвердили встречу, отказались от встречи и так далее).
  • Автоматически проставлять статус 📆 На встрече, когда на встрече.

Проблема

Плагин YandexCalendar(plugin) местами не работает

Решение

Описание

  • Программа представлена в виде интеграции сервиса mattermost и яндекс календаря по протоколу CalDAV, а именно, вытягивание конференций из сервера.
  • Доступны запросы на получение списка конференций с атрибутами в различные промежутки времени.
  • Аутентификация по логину яндекса и токену яндекс приложения.
  • Настройка часового пояса.
  • Ежедневное уведомление в заданное время по часовому поясу пользователя.
  • Уведомление о предстоящей конференции за 10 минут до начала.
  • Установка статуса "📆 In a meeting" во время конференции. Если изначальный статус длительнее или в режиме не стирать, то происходит возврат вашего статуса после конференции. Напрмер: 🏠 "Working from home". Status Don't clear -> 📆 In a meeting. Status 18:30 -> 🏠 "Working from home". Status Don't clear.
  • Проверка активности аккаунта.
  • Проверка активности планировщика.
  • Возможность удалить/обновить установленные параметры в интеграции
  • Уведомление об изменении/добавлении/удалении конференций, наличие всех доступных атрибутов участников конференции
  • Интеграция с несколькими яндекс календарями по выбору

Предварительные требования

  • Docker Engine 24.0.5
  • Python 3.12

Дерево команд

/yandex_calendar
.
├── calendars
│         ├── current
│         ├── from_to
│         ├── get_a_month
│         ├── get_a_week
│         └── today
├── checks
│         ├── check_account
│         └── check_scheduler
├── connections
│         ├── connect
│         ├── disconnect
│         ├── update
│         └── profile
├── info
└── notifications
    ├── create
    ├── delete
    └── update

Команды

  • yandex_calendar [root all commands in integration app]
    • connections [module authentication client]

      • connect [connection to yandex calendar, need required login, token yandex app, timezone - form command]
      • disconnect [disconnection account from integration - execute command]
      • update [update login, token, timezone - form command ]
      • profile [show info about me(user attributes) - execute command]
    • calendars [module Yandex Calendar API for client]

      • get_a_week [get conferences for the week by user timezone, execute command]
      • get_a_month [get conferences for the month by user timezone, execute command]
      • current [get conferences on current day by user timezone, need dd.mm.YYYY - form command]
      • from_to [get conferences from date dd.mm.YYYY to date dd.mm.YYYY by user timezone, need start date and end date in format dd.mm.YYYY - form command]
      • today [get conferences for today by user timezone, execute command]
    • notifications [module scheduler with notifications]

      • create [create jobs with notifications every day or/and every next conference before in 10 minutes, need select calendar with exists conferences, select time 00:00->23:45 with interval 15 minutes(required), click Notification for every next conferences notifications(optional), click Status for change status when in a meeting]
      • update [clear user jobs(scheduler) and create all by command create again]
      • delete [clear user jobs(scheduler)]
    • checks [module checking info about active user]

      • check_account [check exist user in integration]
      • check_scheduler [check exist notifications for user in integration ]
    • info [help information about commands app, execute command]

Получить токен

  1. Перейти на сайт Яндекс ID
  2. Войти или зарегистрироваться
  3. Перейти в Безопасность
  4. В самом низу Пароли приложений
  5. Нажмите на Календарь CalDAV и создайте пароль
  6. Для интеграции потребуется логин яндекс аккаунта и созданный ранее пароль

Или можно почитать первый шаг из https://yandex.ru/support/calendar/common/sync/sync-desktop.html

Запуск

  • Клонируем репозиторий

    git clone https://github.com/ArtemIsmagilov/mm-yc-notify && cd mm-yc-notify/
  • Активируем виртуальное окружение

    python3 -m venv venv && source venv/bin/activate
  • Копируем файл с переменными окружения и редактируем в зависимости от ваших общих конфигураций

    cp .example.env .env
  • Настраиваем переменные wsgi/settings.py

  • Настраиваем переменные gunicorn.conf.py

  • Настраиваем microservice/docker-compose.yml

  • Запускаем докер контейнер mattermost

    cd ./mm/
    sudo docker compose up
  • Запускаем докер контейнер приложения

    сd ../microservice
    sudo docker compose up
  • Устанавливаем бота в mattermost, а именно - пишем команду в любом диалоговом окне

    /app install http http://192.168.31.57:8065/manifest.json
    
  • Теперь необходимо добавить бота в команду https://www.ibm.com/docs/en/z-chatops/latest?topic=platform-inviting-created-bot-your-mattermost-team

  • Создать токен, предоставить права

  • В файле .env добавить токен для приложения MM_APP_TOKEN=example

  • Перезапустить докер контейнер

    sudo docker compose -f ./microservice/docker-compose.yml down
    sudo docker compose -f ./microservice/docker-compose.yml up
  • При разработке, удобно запустить отдельно postgres и app

    • postgres

      cd /psql
      sudo docker compose up
    • app

      cd ../
      flask init-db
      bash run-app.bash

Тесты

  • предварительно у вас должен быть запущен mattermost, psql контейнеры, у бота должны быть токен и права.
  • очищаем БД в src/
    sudo quart init-db -c
  • запускаем тесты в папке src
    coverage run -m pytest --cache-clear
    coverage report -m
      Name                                                Stmts   Miss  Cover   Missing
      ---------------------------------------------------------------------------------
      src/app/__init__.py                                    48      2    96%   43, 47
      src/app/app_handlers.py                                14      0   100%
      src/app/async_wraps/async_wrap_caldav.py               27      3    89%   17, 24-25
      src/app/bots/bot_commands.py                           34      4    88%   12, 33, 50, 55
      src/app/calendars/caldav_api.py                        97     14    86%   73-95, 180
      src/app/calendars/caldav_filters.py                    10      6    40%   10-14, 18, 22
      src/app/calendars/caldav_funcs.py                      15      2    87%   23-25
      src/app/calendars/caldav_searchers.py                  21      5    76%   41-45
      src/app/calendars/calendar_app.py                      15      0   100%
      src/app/calendars/calendar_backgrounds.py              25      8    68%   22-28, 32-33
      src/app/calendars/calendar_views.py                    11      1    91%   12
      src/app/calendars/conference.py                        96      9    91%   25, 35, 50, 60, 90, 103, 118, 123, 170
      src/app/checks/check_app.py                             9      0   100%
      src/app/checks/check_my_account.py                     13      0   100%
      src/app/checks/check_my_scheduler.py                   14      0   100%
      src/app/connections/connection_app.py                  24      0   100%
      src/app/connections/connection_backgrounds.py          40      7    82%   46, 49-56, 71, 92
      src/app/connections/connection_handlers.py             62      0   100%
      src/app/constants.py                                    3      0   100%
      src/app/converters.py                                  80     46    42%   25-26, 36, 40-42, 46-47, 52-54, 61-76, 83-94, 98-103, 107, 117-120, 124, 132, 136
      src/app/decorators/account_decorators.py               66      5    92%   74-75, 87-89
      src/app/dict_responses.py                              48      7    85%   49, 106, 120, 127, 148, 155, 162
      src/app/notifications/notification_app.py              24      0   100%
      src/app/notifications/notification_backgrounds.py      46      3    93%   54-56, 94
      src/app/notifications/notification_handlers.py         99      4    96%   47, 122, 184, 261
      src/app/notifications/notification_views.py            23      7    70%   20, 37-40, 53-56
      src/app/notifications/tasks.py                        214    103    52%   66-67, 73-74, 80-81, 87-88, 94-95, 107-108, 122, 138, 157-205, 220, 245-300, 313, 320, 357-456, 462-476
      src/app/notifications/worker.py                        14      0   100%
      src/app/schemas.py                                     31      0   100%
      src/app/settings.py                                    39      0   100%
      src/app/sql_app/crud.py                                87     10    89%   154, 178-188, 192-199, 249, 266-277, 281
      src/app/sql_app/database.py                             8      0   100%
      src/app/sql_app/db_CLI.py                              22     10    55%   20-25, 29-30, 34-36
      src/app/sql_app/models.py                               5      0   100%
      src/app/validators.py                                   8      2    75%   9-10
      src/tests/__init__.py                                   0      0   100%
      src/tests/additional_funcs.py                          33      2    94%   62-63
      src/tests/conftest.py                                  64      4    94%   47, 69, 73, 112
      src/tests/test_app.py                                 654      0   100%
      ---------------------------------------------------------------------------------
      TOTAL                                                2143    264    88%
    
    Подробная информация в html
    coverage html
    Помимо тестов также требуется соблюдение PEP8
    flake8 src/

Интеграция в производственной среде

  • получить сертификаты для зашифрованного трафика https://certbot.eff.org/
  • развернуть докер контейнер маттермоста с HTTPS https://docs.mattermost.com/install/install-docker.html
  • в wsgi/gunicorn.conf.py меняем протокол на https, хост и порт на локальные 127.0.0.1:5000 , добавляем сертификаты и указываем пути к ним. Настраиваем количество работников и потоков. В зависимости от вашей кофигурации, меняем переменные в .env
  • добавляем nginx конфигурацию для проксирования запросов к интеграции на локальный хост и порт
    sudo nano /etc/nginx/sites-enabled/app_nginx
    # in /etc/nginx/sites-enabled
    # reverse proxy app
    # http
      
    #server {
    #    listen 10081;
    #    listen [::]:10081;
    #    location / {
    #        include proxy_params;
    #        proxy_pass http://127.0.0.1:5000/;
    #    }
    #}
      
    # https
    server {
        listen 10441 ssl; # managed by Certbot
        listen [::]:10441 ssl;
        ssl_certificate /etc/letsencrypt/live/CHANGE/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/CHANGE/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
        server_name CHANGE; # managed by Certbot
      
        location / {
            include proxy_params;
            proxy_pass https://127.0.0.1:5000/;
        }
    }
    
    sudo systemctl reload nginx
  • После добавления интеграции не забудьте создать токен, предоставить права и добавить в src/.env
  • Перезапустить докер контейнер.

Как работает

  • /yandex-calendar checks check_account checks_account

  • /yandex-calendar connections connect connect

  • /yandex-calendar connections profile profile

  • /yandex-calendar calendars current [--date] current

  • /yandex-calendar connections disconnect disconnect

  • /yandex-calendar calendars get_a_month
    Снизу будут высвечиваться ошибки клиента и разработчика, в данном случае пользователь не авторизовался, но пытается получить список конференций за месяц. error

  • /yandex-calendar calendars from_to from_to

  • /yandex-calendar calendars get_a_month
    В модуле calendars список конференций в различных интервалах будет в следующей форме get_a_month

  • /yandex-calendar info info

  • /yandex-calendar notifications create/update
    Вы можете создать только один планировщик. С помощью update можно обновить планировщик, задать другие параметры для уведомлений notifications_create

    Ответное сообщение о созданном планировщике notification_created

  • /yandex-calendar notifications delete
    Попытка удалить несуществующий планировщик notifications_delete

  • Уведомление (за 10 минут до конференции)/(ежедневное обо всех сегодня конференциях) notify

Алгоритм уведомлений

  1. В переменной окружения следует прописать в стиле cron частоту запроса от каждого пользователя, который подключить планировщик, к яндекс-серверу
    Примеры https://crontab.guru/examples.html

  2. Внимание! Администратору следует тонко настроить время повторения пинга сервера. Если запросы слишком регулярные, то приложение будет излишне перегружено, если же редкие, то для сотрудников с очень частыми конференциями информация от уведомлений будет не актуальна.

  3. Вы получаете уведомление об удалённых/изменённых/добавленных конференций на яндекс-сервере.

  4. При тестировании следует поменять несколько параметров, а именно:

    1. поменять в коде функцию уведомление за 1 минуту(поменять 10 на 1)
    2. поменять функцию, которая проверяет существование конференции в пределах от 15 минут и более на в переделах от времени сейчас и более.
    3. поменять параметры создания задач на пинг сервера яндекс календаря с cron стилистики на параметр секунды, например пинг каждые 10 секунд, в продакшене этого не стоит делать. Также добавить параметр случайности. jitter - исполнение задач в случайное время в заданном порядке секунд https://apscheduler.readthedocs.io/en/latest/modules/triggers/cron.html#module-apscheduler.triggers.cron . Это обеспечивает стабильность при сильных нагрузках в час пик.
  5. Как работает:

    1. Отслеживаются приложением все конференции и при совершении над ними операция приходят уведомления
    2. Приложение уведомляет вас о предстоящей конференции за 10 минут до начал при условии, что конференция была добавлена не раньше 15 минут. Почему? Потому что нужно гарантированно отправлять уведомления о событиях которые не были удалены за 10 минут до сообщения. 5 минут буфер.
    3. Смена статус осуществляется в момент начала конференции и закачивается в момент окончания конференции. Если, ваш статус длительнее по времени статуса конференции или стоит в режиме(постоянно), то приложение вернут ваш статус.
    4. Блок notifications состоит из подпунктов:
      • уведомление ежедневные в конкретное время
      • уведомление обо всех операция связанных с вашими конференциями, синхронизация через лонг-пуллинг, зависит от настройки администратора
      • уведомление за 10 минут до начала предстоящей конференции
    5. Примеры:
      • добавленная конференция was_added
      • измененная конференция was_updated
      • удалённая конференция was_deleted
  6. Предостережение

    • Если клиент при создании интеграции вводите валидный логин, токен, потом пользуетесь приложением, затем меняете в яндекс календаре логин или токен, то интеграция сразу удаляет ваши данные из приложения как невалидного пользователя. Следует либо ничего не менять, либо в приложении удалять/обновлять данные через команды приложения.

    • Если вы выбираете некоторые календари, а потом удаляете в яндекс календаре, ваш изначальный выбор уведомлений с синхронизированными конференциями удаляется как не валидные данные, опять же - обновляйте/удаляте ваши изменения через команды приложения.

    • Конференции, которые были добавлены раньше 15 минут не будут обрабатываться приложением для обновления статуса и сообщения о предстоящем событии, так как невозможно уведомить о предстоящей конференции за 10 минут если вы создали конференцию за 1, 3, 5 и так далее до 15 минут. 5 минут добавлены как буфер.

    • библиотека apscheduler не корректно работает с несколькими процессами, поэтому в настройках gunicorn не следует запускать более 1 процесса. Существуют обходные пути, которые позволяют работать с несколькими потоками в apscheduler.

      APScheduler does not currently have any interprocess synchronization and signalling scheme that would enable the scheduler to be notified when a job has been added, modified or removed from a job store.

      Workaround: Run the scheduler in a dedicated process and connect to it via some sort of remote access mechanism like RPyC, gRPC or an HTTP server. The source repository contains an example of a RPyC based service that is accessed by a client.

Диаграммы

  • Пинг сервера
    diagram
  • БД
    DB
  • apscheduler_job
    tb1
  • user_account
    tb2
  • yandex_calendar
    tb3
  • yandex_conference
    tb4

Пробуем новую архитектуру

  • Повысили эффективность и производительность кода сделав его асинхронным(asyncio, Quart).
  • Добавили брокер сообщений rabbitmq через который очень быстро и надёжно передаются сообщения. (Раньше просто БД)
  • Внедрили библиотеку фоновых задач Dramatiq(Для наших задач самое то)
  • Есть идея хранить данные синхронизированных календарей и конференций в Redis так как обмен данными очень быстрый, но структура данных не совсем ключь-значние(пока думаем)
  • Требуется нагрузочное и mock тестирование
  • Можно удалить middleware Prometheus в dramatiq(в 2 версии автор собирается убрать из коробки)
  1. rabbitmq Добавляем переменные окружения в .env
    sudo docker compose up -d
  2. worker dramatiq
    dramatiq app.notifications.tasks
  3. events scheduler
    python -m app.notifications.task0_scheduler
  4. web server
    bash run-server.bash
  5. После проверки корректности работы всех компонентов добавляем логирование
  6. Запускаем докер файл
    cd src/microsevice/ && sudo docker compose up -d
    Cмотрим статистику
    sudo docker stats 
    Смотрим логи
    sudo docker compose logs > output.txt
  7. Можно добавить миграцию БД
    • https://alembic.sqlalchemy.org/en/latest/tutorial.html
    • https://alembic.sqlalchemy.org/en/latest/autogenerate.html
    • sqlalchemy/alembic#805
    • в /src
      alembic init -t async alembic
    • в файле alembic.ini меняем url на действительный
      sqlalchemy.url = driver://user:pass@localhost/dbname
    • в файле env.py меняем target_metadata
      from app.sql_app.models import metadata_obj
      target_metadata = metadata_obj
      # target_metadata = None
      
    • при изменениях делаем автогенерацию скрипта миграции
      alembic revision --autogenerate -m "Added account table"
    • запускаем миграцию
      alembic upgrade head
    • получить информацию
      alembic history --verbose

Полезные ссылки

Вопрос-ответ

  • Почему не шифруешь токены?

    • Изначально шифровал с помощью библиотеки cryptography через объект Fernet. В процессе разработки выяснил, что ни Redmine, ни Mattermost не шифруют токены. Хотя пароли хэшируют - это мы одобряем. В интернете большая полемика по тому, как правильно шифровать личную информацию. Я склонился к тому, что нужно придерживаться политика самого сайта, где рядом создаётся интеграция. Если шифруют, то нужно тоже шифровать и наоборот.

    • Давайте посмотрим в как токен храниться в нашей БД.

      • вы должны уже были авторизоваться в интеграции через маттермост(добавить логин, токен, часовой пояс).
      sudo docker exec -it microservice-db-1 bash

      Вы перешли в оболочку контейнера БД, смотрим содержимое БД

      su postgres
      psql
      \dt
                    List of relations
       Schema |       Name        | Type  |  Owner   
      --------+-------------------+-------+----------
       public | user_account      | table | postgres
       public | yandex_calendar   | table | postgres
       public | yandex_conference | table | postgres
      (3 rows)
      
      SELECT id, token FROM user_account;
       id |      token         
      ----+----------------------------
        1 | hak3jkeh67y4dd1h6q8r16gqrr 
      (1 row)
      
    • Токен у нас не зашифрован(мы ничего и не делали для этого), предыдущий метод долгий, лучше просто добавить в докер контейнер маттермоста adminer и посмотреть в вебе. mm

    • В маттермост тоже не зашифрован, такая же ситуация и в редмайне.

    • Почему не шифруем? Если, буквально в двух словах, то REST API токены позволяем использовать совсем малую часть функционала приложений. Пароли захешированы, уже хорошо. Если унесут БД все токены можно сразу же поменять.

    • https://docs.mattermost.com/developer/personal-access-tokens.html

  • Что мы тестируем?

    • Тесты проверяют правильность работы клиентской части - разделы
      1. /calendars
      2. /checks
      3. /connections
      4. /notifications
      5. /jobs

Проблемы Яндекс Календаря

  • Отсутствие какой-либо документации, RESTAPI, туториала, инструментария от яндекса для работы с Яндекс-Календарём в удобном формате JSON, XML. Google хорошо задокументировал работу с google календарём на python https://developers.google.com/calendar/api/quickstart/python?hl=ru
  • Выборка текущих конференций происходит по часовому поясу клиента, если вы поменяли часовой пояс, то старые конференции не меняют часовой пояс, только на сайте визуально. Поэтому нужно настроить часовой пояс один раз при создании клиента интеграции в маттермост.
  • Не доступны запросы на занятость (тег freebusy, статус 504).
  • Если в яндекс-календаре создаю календари с одинаковым именем, то при добавлении в форму, маттермост удаляет эти дубликаты. Это и логично, как нам их отличить, если одинаковые имена. Можно сделать выбор по id, но это не самые комфортный интерфейс для пользователя.
  • Произвольные обновления событий без участия клиента, что требует дополнительной корректировки синхронизации событий. Скорее всего, сервер обновляет свойства событий не относящиеся к атрибутам вытягиваемой конференции. Для правильной синхронизации потребовалась перепроверка на идентичность новых событий-конференций по sync_token с событиями-конфренециями в БД.
  • Не корректное хранение данных о событии. Здесь имеется ввиду, что при поиске событий через метод calendar.search возвращаются не отсортированные и лишние события. Я создал issue для автора библиотеки caldav python-caldav/caldav#351 . Тут не понятно, то ли это проблема Яндекс Календаря(не правильно сохраняет timezone) то ли библиотеки caldav

Будущие доработки

  • добавить русский язык
  • оптимизировать алгоритм обновлений конференций через CalDAV сервер
  • покрыть код тестами
  • выявить уязвимости
  • желательна переработка кода
  • добавить автоматизацию тестов и деплоя на сервер (CI/CD) через Github workflows

Благодарности

About

Mattermost - Yandex Calendar Integration(CalDAV)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages