- PVSM.RU - https://www.pvsm.ru -

ХрюХрюКар v.2 или как я использую Go для защиты своего двора

ХрюХрюКар v.2 или как я использую Go для защиты своего двора - 1

Привет!

При этом я по-максимуму старался вести разъяснительную работу с нарушителями. Например, нанес [1] на карту все стоянки в городе, предварительно их обзвонив и убедившись в том, что 29 из 30 стоянок в городе заполнены меньше чем на половину. Также пытался донести до автомобилистов простую истину: во дворах сложившейся застройки нет места [2] для их автомобиля.

О результатах эксперимента и некоторых выводах я подробно написал в своеобразном постмортеме [3] первой версии ХХК. Опять же, если кратко, то выводы просты - чтобы был результат, нужно в десяток раз больше штрафов: необходимо исключать наказание в виде предупреждения, увеличивать размер штрафов и повышать продуктивность работы комиссий, чтобы с момента фиксации нарушения до момента привлечения к ответственности проходило не более пары недель (сейчас этот показатель - 1,5-2 месяца).

В процессе, я пришел к выводу что для решения данной проблемы нам в Балаково нужен ПАК "Помощник Москвы" [4], однако внедрять столь остро-социальное решение наш регион пока не готов, по понятным причинам. В общем оставалось только ждать и терпеть.

Терпел я до весны, пока не увидел в своем, только что благоустроенном дворе, вот это:

Там стояло три автомобиля. Собственники живут в этом доме и их не смущает тот факт, что своей ленью они превращают свой же дом в свинарник.

Там стояло три автомобиля. Собственники живут в этом доме и их не смущает тот факт, что своей ленью они превращают свой же дом в свинарник.

А еще вот такое попалось [5] (в 9 минутах ходьбы от полупустой стоянки):

Все они оправдывают себя тем, что мест на стоянках нет, хотя это не так. В 9-и минутах ходьбы от этой локации есть стоянка, заполненная лишь на 40%.

Все они оправдывают себя тем, что мест на стоянках нет, хотя это не так. В 9-и минутах ходьбы от этой локации есть стоянка, заполненная лишь на 40%.

В общем я осознал что все эти "ребята" меня дико триггерят и с этим надо что-то делать уже сейчас.

Почему v.2?

Просто так запустить ХХК на этот раз не получилось по двум основаниям:

1) Госдума решила запретить [6] использование электронной почты для подачи обращений граждан. В первой версии ХХК обращения направлялись как раз по ЭП.

2) Одним из узких мест в модели работы ХХК было решение, по которому я (или модератор), проверяли все материалы и подписывались под каждым заявлением. Мы многое там автоматизировали, но все-же это одно из самых узких мест в системе: мне приходилось тратить по часу в день, чтобы разобрать сотню фотографий и направить обращения в комиссию и ГИБДД.

Мне давно хотелось научиться пилить ботов в телеграме и хотелось сделать какой-нибудь проект на Го с нуля, поэтому было принято решение сделать абсолютно новую версию ХХК, которая будет представлять собой телеграм-бота.

Кейс на этот раз такой:

  1. Пользователь делится с ботом местоположением и присылает ему снимок автомобиля на зелёной зоне;

  2. Бот распознает автомобильные номера, производит обратное геокодирование;

  3. Пункт 1-2 повторяется столько раз, сколько требуется.

  4. Пользователь переходит в режим утверждения снимков, где может поправить номера, адрес или точку, либо вовсе удалить снимок.

  5. Когда все или часть снимков утверждены, пользователь переходит в секцию подачи обращения.

  6. Бот формирует одно обращение на все утвержденные снимки пользователя, сделанные в одном городе. По результатам бот выдает текст обращения и PDF-файл с материалами фотофиксации нарушений. Также бот выдает ссылку на ПОС Госуслуг административной комиссии города (вот [7], например, ПОС для Балаково).

  7. Пользователь сохраняет PDF, копирует текст обращения и переходит на ПОС. Там вставляет текст обращения, прикрепляет PDF и, авторизовавшись через ЕПГУ, отправляет обращение.

В результате на этот раз я ничего не модерирую, при этом никаких чувствительных данных не получаю и не храню (раньше я хранил данные модераторов, которые подписывались под обращениями). Пользователи просто получают удобный инструмент и работают с ним тогда, когда им требуется.

Что получилось

Исходники

В результате, за 2,5 недели удалось написать законченный проект, который я опубликовал под MIT в своем репозитории. [8] Он готов к запуску как локально на слабой машине, так и в продуктовой среде под нагрузкой.

Как выглядит бот

Фиксация нарушений

Утверждение снимков

Подготовка и отправка обращения

Что под капотом?

В продакшене на данный момент обеспечивают работу следующие сервисы/системы:

  • Один инстанс MySQL8

  • Один инстанс Redis для Pub/Sub

  • Два инстанса Django для админ-панели, миграций и генерации PDF [9].

  • Один инстанс Nomeroff за Flask [10] для распознавания номеров (дает около 30-и снимков в секунду, чего мне более чем достаточно)

  • Три инстанса сервера бота [11], написанного на Go

  • Traefik - балансирует запросы [12]телеграма на серверы бота. Получает SSL-сертификаты, проксирует мои запросы к админ-панели. Роутит запросы к внутреннему эндпоинту [13] генерации PDF (и балансирует запросы к нему), также прикрывает за базовой авторизацией (как доп.мера) служебные эндпоинты.

  • S3 Яндекс-облака хранит все снимки и PDF-файлы обращений

Бота можно достаточно быстро запустить локально на своей машине. Процесс запуска я описал в Readme [8].

При запуске в dev-окружении, бот запустится [14] в режиме Long-polling, что позволит вам работать локально, не имея облачной инфраструктуры (кроме S3) и публично доступного домена.

Тем не менее, следует понимать, что для продуктовой среды такой режим не подойдет, поэтому для staging/prod бот запускается [15] в режиме вебхуков, что позволяет горизонтально масштабировать сервис и балансировать нагрузку.

Работа с данными

Для работы с данными я реализовал [16] систему сторов. Все чтение данных производится [17] через стор-кэш, при этом если происходит промах, мы получаем [18] данные из базы, сохраняем в кэш и публикуем [19] апдейт через редис, чтобы другие инстансы тоже положили [20] к себе эти данные.

Данные живут в кэше в соответствии с выставленными TTL и периодически вычищаются [21] из кэша в случаях, если их никто давно не запрашивал. Для возможности последующей оптимизации я во всех сторах использую метрики [22], что позволит в последующем подобрать и выставить правильные TTL для каждого стора [23].

Для изменения данных, запись предварительно необходимо получить в транзакции через специальные методы [24], чтобы обеспечить атомарность изменений и возможность отката транзакции средствами БД. Ну и сохранять [25]/удалять [26] объекты необходимо в той же транзакции.

Есть данные [27], которые я захотел хранить только в ОЗУ серверов, как из соображений безопасности, так и по соображениям производительности. Например, при фиксации нарушений, пользователь может начать делиться [28] своим местоположением, которое телеграм будет присылать мне каждые 30 секунд и которое мне необходимо хранить временно, пока не прилетят обновленные координаты или пользователь не завершит фиксацию нарушений. Также есть некритичная информация, например - текущая секция бота, в которой находится юзер. Все это сохранять в базу не хотелось, при этом было желание хранить это в структурах Go.

По результату, я реализовал схожий набор сторов, где все данные хранятся только в кэше [29], а redis помогает временно блокировать [30] данные по идентификатору на короткое время для изменений. После внесения изменений, их автор, владея токеном блокировки, производит сохранение [31], сообщая об обновлении другим инстансам и снимая блокировку.

Бот

Для реализации сервера бота [32]я использовал github.com/mymmrac/telego [33]. API оказался достаточно прост для понимания.

По сути, для реализации бота необходимо реализовать:

Каждый обработчик и промежуточный слой, помимо самой функции, содержит еще и предикат [40], который должен возвращать булево значение. Это значение будет определять - запускать обработчик или нет. В предикате не следует делать какие-либо запросы к БД, иначе эти запросы будут выполняться каждый раз, когда сервер будет обходить хендлеры поочередно и определять какой из них следует выполнять, а какой - нет.

Инлайн-клавиатуры оказались достаточно удобны тем, что каждой кнопке можно назначить [41] callback-данные, которые при нажатии на нее, прилетят в наш обработчик. Однако Telegram не готов брать на себя хранение наших данных в объеме, большем чем 64 байта на callback-данные одной кнопки, поэтому имейте это в виду, используя UUID в качестве идентификаторов, ведь название команды и один идентификатор в 64 байта вы еще сможете уместить, а вот два - уже нет...

Иное

Для удобства был реализован следующий набор [42] внутренних пакетов:

  • config [43] - для управления конфигурациями. Использовал github.com/kelseyhightower/envconfig [44];

  • encryptor [45] - для простого детерминированного шифрования tg-идентификаторов пользователей, чтобы не хранить их в открытом виде;

  • geocoder [46] - для обратного геокодирования (получение адреса по координатам). Используется сервис Nominatim от OSM;

  • logger [47] - журналирование данных в структурированном виде. Спасибо @JustSkiv [48] за код.

  • pdf [49] - клиент генератора приложений к обращению в формате PDF. Пример приложения [50].

  • recogniser [51] - клиент сервиса распознавания номеров.

  • storage [52] - хранение данных в S3. Реализованы варианты для публичных бакетов и приватных (с подписью в ссылке).

  • transactions [53] - удобная обертка для создания транзакций с несколькими ретраями в случае retryable-ошибок. Пример [54] использования.

Для сборки и управления зависимостями используется bazelisk. В dev-окружении используется iBazel, который сам следит за изменениями go-файлов и быстро пересобирает проект если есть изменения.

Ссылки

Репозиторий проекта [8];

Первая (старая) версия [55] проекта от 2024 года (Django+Vue);

Бот проекта [56] (работает по крупнейшим городам Саратовской области, но могу оперативно добавить любой город РФ, пишите на почту [57]).

Канал проекта [58].

P.S.

Спасибо за уделенное время! Надеюсь что мой опыт поможет кому-нибудь запустить миграцию автомобилей с зеленых зон своего двора на стоянки и в гаражи.

Если будут какие-либо вопросы или замечания по коду - пишите. Код далек от идеала и, конечно, требует внимания и доработки.

Ну и хотел сказать спасибо @JustSkiv [48] за его публикации. Пользуясь случаем, рекомендую его TG-канал [59]для тех кто также как и я влюбился в Go. Там теплая, ламповая атмосфера и классные тематические стикеры:

ХрюХрюКар v.2 или как я использую Go для защиты своего двора - 4

Автор: lepekhovs

Источник [60]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/telegram/416822

Ссылки в тексте:

[1] нанес: https://t.me/oinkoinkcar/120

[2] нет места : https://t.me/oinkoinkcar/142

[3] постмортеме: https://vk.com/@xxkap_blk-ps

[4] ПАК "Помощник Москвы": https://t.me/oinkoinkcar/137

[5] вот такое попалось: https://t.me/oinkoinkcar/207

[6] решила запретить: https://t.me/oinkoinkcar/204

[7] вот: https://pos.gosuslugi.ru/form/?opaId=165&fz59=false

[8] в своем репозитории.: https://gitlab.com/theansweris42/xxk

[9] генерации PDF: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/django_backend/pdf_api/views.py

[10] Nomeroff за Flask: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/nomeroff/main.py

[11] сервера бота: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/bin/server/server.go#L83

[12] балансирует запросы : https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/traefik/conf-prod/traefik-conf-prod-000.yml#L90

[13] внутреннему эндпоинту: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/traefik/conf-prod/traefik-conf-prod-000.yml#L48

[14] запустится: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/bot.go#L224

[15] запускается: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/bot.go#L215

[16] реализовал: https://gitlab.com/theansweris42/xxk/-/tree/main/go_backend/internal/store?ref_type=heads

[17] производится: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L145

[18] получаем: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L364

[19] публикуем: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L379

[20] положили: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L588

[21] вычищаются: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L546

[22] метрики: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L49

[23] TTL для каждого стора: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/cache.go

[24] методы: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/store.go#L26

[25] сохранять: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/store.go#L23

[26] удалять: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/store.go#L25

[27] данные: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/models/telegram_user_state.go

[28] начать делиться: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/middlewares.go#L390

[29] в кэше: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/telegram_user_state.go

[30] блокировать: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/redis_broker/telegram_user_state.go#L53

[31] сохранение: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/telegram_user_state.go#L189

[32] сервера бота : https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/bot.go

[33] github.com/mymmrac/telego: http://github.com/mymmrac/telego

[34] команд: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/commands.go

[35] сообщений: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/messages.go

[36] клавиатур: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/keyboards.go

[37] сообщений: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go

[38] обратных вызовов: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go#L21

[39] промежуточных слоев: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/middlewares.go

[40] предикат: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go#L490

[41] назначить: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/keyboards.go#L37

[42] набор: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib

[43] config: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/config/config.go

[44] github.com/kelseyhightower/envconfig: http://github.com/kelseyhightower/envconfig

[45] encryptor: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/encryptor

[46] geocoder: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/geocoder

[47] logger: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/logger/logger.go

[48] @JustSkiv: https://www.pvsm.ru/users/justskiv

[49] pdf: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/pdf/pdf.go

[50] Пример приложения: https://storage.yandexcloud.net/oinkoinkcar/example-app.pdf

[51] recogniser: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/recogniser

[52] storage: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/storage/storage.go

[53] transactions: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/transactions/transactions.go

[54] Пример: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go#L536

[55] Первая (старая) версия: https://gitlab.com/theansweris42/oinkoinkcar

[56] Бот проекта: https://t.me/XXKapBot

[57] на почту: mailto:lepehovsv@gmail.com

[58] Канал проекта: https://t.me/oinkoinkcar

[59] TG-канал : https://t.me/ntuzov

[60] Источник: https://habr.com/ru/articles/901082/?utm_source=habrahabr&utm_medium=rss&utm_campaign=901082