diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..553ac51 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,20 @@ +kind: pipeline +type: exec +name: default + +platform: + os: linux + arch: amd64 + +steps: + - name: build and push + commands: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - make docker_release + when: + branch: + - master + +trigger: + event: + - push diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..20bafbf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:1.22-alpine AS builder +WORKDIR /app +COPY ./ ./ +RUN set -eux; \ + apk add --no-cache bash make git dumb-init mailcap ca-certificates tzdata; \ + update-ca-certificates; \ + make + +FROM scratch +WORKDIR / +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=builder /etc/mime.types /etc/mime.types +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /usr/bin/dumb-init /bin/dumb-init +COPY --from=builder /app/build/vegapokerbot /vegapokerbot +EXPOSE 3333 +ENTRYPOINT ["/bin/dumb-init", "--"] +CMD ["/vegapokerbot", "run"] diff --git a/Makefile b/Makefile index a554761..4a64f9d 100644 --- a/Makefile +++ b/Makefile @@ -5,12 +5,19 @@ ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) BIN=$(ROOT_DIR)/build/vegapokerbot GO_VERSION=$(shell go version | sed -e 's/go version //') BIN_DIR=$(ROOT_DIR)/build +APP_IMAGE=gitea.neur0tx.site/neur0toxine/vegapokerbot:latest build: deps fmt @echo "> building with ${GO_VERSION}" @CGO_ENABLED=0 go build -buildvcs=false -tags=release -o $(BIN) . @echo $(BIN) +docker: + @docker buildx build --platform linux/amd64 --tag ${APP_IMAGE} --file Dockerfile . + +docker_release: + @docker buildx build --push --platform linux/amd64 --tag ${APP_IMAGE} --file Dockerfile . + run: @${BIN} run fmt: diff --git a/compose.prod.yml b/compose.prod.yml new file mode 100644 index 0000000..e0e93e2 --- /dev/null +++ b/compose.prod.yml @@ -0,0 +1,27 @@ +services: + db: + image: postgres:latest + environment: + POSTGRES_USER: app + POSTGRES_PASSWORD: app + POSTGRES_DATABASE: app + networks: + - default + + app: + image: "gitea.neur0tx.site/neur0toxine/vegapokerbot:latest" + networks: + - default + links: + - db + env_file: + - .env + labels: + traefik.enable: "true" + traefik.http.routers.vegapokerbot.entrypoints: web + traefik.http.routers.vegapokerbot.rule: "Host(`example.com`) && PathPrefix(`/webhook`)" + traefik.http.services.vegapokerbot.loadbalancer.server.port: 3333 + +networks: + default: + driver: bridge diff --git a/internal/config/config.go b/internal/config/config.go index bf29f17..01b8376 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,9 +18,7 @@ type Config struct { func LoadConfig() (*Config, error) { var cfg Config - if err := godotenv.Load(); err != nil { - return nil, err - } + _ = godotenv.Load() loader := aconfig.LoaderFor(&cfg, aconfig.Config{ SkipFlags: true, EnvPrefix: "POKER_BOT", diff --git a/internal/handler/callback_query_handler.go b/internal/handler/callback_query_handler.go index 4e80fa4..e575a46 100644 --- a/internal/handler/callback_query_handler.go +++ b/internal/handler/callback_query_handler.go @@ -367,7 +367,7 @@ func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, chatID in MessageID: msgID, Text: loc.Message("redmine_will_not_be_configured"), }) - return util.SendSetupDone(h.App.TG(), user.ChatID, loc) + return util.SendSetupDone(h.App.TG(), h.App.TGProfile().Username, user.ChatID, loc) } _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), @@ -428,7 +428,7 @@ func (h *CallbackQueryHandler) handleAnswerRedmineSendResults(answer util.Answer MessageID: msgID, Text: loc.Message("redmine_poker_will_not_be_configured"), }) - return util.SendSetupDone(h.App.TG(), user.ChatID, loc) + return util.SendSetupDone(h.App.TG(), h.App.TGProfile().Username, user.ChatID, loc) } _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{ ChatID: tu.ID(user.ChatID), diff --git a/internal/handler/chat_member_updated_handler.go b/internal/handler/chat_member_updated_handler.go index 3441c99..fb785cc 100644 --- a/internal/handler/chat_member_updated_handler.go +++ b/internal/handler/chat_member_updated_handler.go @@ -52,8 +52,9 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error { } if user == nil || user.ID == 0 { _, err = h.App.TG().SendMessage(&telego.SendMessageParams{ - ChatID: tu.ID(tgChat.ID), - Text: h.Localizer(language.English.String()).Message("you_should_register_first"), + ChatID: tu.ID(tgChat.ID), + Text: h.Localizer(language.English.String()). + Template("you_should_register_first", map[string]interface{}{"Name": h.App.TGProfile().Username}), ParseMode: telego.ModeMarkdown, }) h.leaveChat(tgChat.ID) diff --git a/internal/handler/new_message_handler.go b/internal/handler/new_message_handler.go index cd8ea5b..5fdd167 100644 --- a/internal/handler/new_message_handler.go +++ b/internal/handler/new_message_handler.go @@ -21,23 +21,27 @@ func (h *MessageHandler) Handle(wh telego.Update) error { if wh.Message.From == nil { return nil } - if wh.Message.Chat.Type == telego.ChatTypePrivate { - if util.MatchCommand("start", wh.Message) { - return wizard.NewRegister(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) - } - if util.MatchCommand("help", wh.Message) { - return wizard.NewHelpCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) - } - setup, found := store.RedmineSetups.Get(wh.Message.Chat.ID) - if found { - return wizard.NewRedmineSetup(h.App, wh.Message.From.ID, wh.Message.Chat.ID, setup).Handle(wh) - } + if util.MatchCommand("start", wh.Message) { + return wizard.NewRegister(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) } if util.MatchCommand("poll", wh.Message) { return group.NewPoll(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) } + if util.MatchCommand("start", wh.Message) { + return wizard.NewRegister(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) + } + + setup, found := store.RedmineSetups.Get(wh.Message.Chat.ID) + if found { + return wizard.NewRedmineSetup(h.App, wh.Message.From.ID, wh.Message.Chat.ID, setup).Handle(wh) + } + + if util.MatchCommand("help", wh.Message) { + return wizard.NewHelpCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) + } + return wizard.NewUnknownCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) } diff --git a/internal/handler/util/common_messages.go b/internal/handler/util/common_messages.go index 65d5816..fc3504a 100644 --- a/internal/handler/util/common_messages.go +++ b/internal/handler/util/common_messages.go @@ -6,10 +6,10 @@ import ( tu "github.com/mymmrac/telego/telegoutil" ) -func SendSetupDone(tg *telego.Bot, chatID int64, loc locale.Localizer) error { +func SendSetupDone(tg *telego.Bot, username string, chatID int64, loc locale.Localizer) error { _, err := tg.SendMessage(&telego.SendMessageParams{ ChatID: tu.ID(chatID), - Text: loc.Message("setup_done"), + Text: loc.Template("setup_done", map[string]interface{}{"Name": username}), }) return err } diff --git a/internal/handler/wizard/help_command.go b/internal/handler/wizard/help_command.go index 51a1fd5..5804169 100644 --- a/internal/handler/wizard/help_command.go +++ b/internal/handler/wizard/help_command.go @@ -16,8 +16,9 @@ func NewHelpCommand(app iface.App, userID, chatID int64) *HelpCommand { func (h *HelpCommand) Handle(wh telego.Update) error { _, err := h.App.TG().SendMessage(&telego.SendMessageParams{ - ChatID: tu.ID(wh.Message.Chat.ID), - Text: h.Localizer(wh.Message.From.LanguageCode).Message("help_output"), + ChatID: tu.ID(wh.Message.Chat.ID), + Text: h.Localizer(wh.Message.From.LanguageCode). + Template("help_output", map[string]interface{}{"Name": h.App.TGProfile().Username}), ParseMode: telego.ModeMarkdown, }) return err diff --git a/internal/handler/wizard/redmine_setup.go b/internal/handler/wizard/redmine_setup.go index ddd6877..366f4a9 100644 --- a/internal/handler/wizard/redmine_setup.go +++ b/internal/handler/wizard/redmine_setup.go @@ -27,6 +27,9 @@ func NewRedmineSetup(app iface.App, userID, chatID int64, config *store.RedmineS } func (h *RedmineSetup) Handle(wh telego.Update) error { + if wh.Message.Chat.Type != telego.ChatTypePrivate { + return nil + } if h.Redmine == nil { return nil } @@ -119,7 +122,7 @@ func (h *RedmineSetup) handleKeyStep(text string, loc locale.Localizer) error { _, _ = h.App.TG().SendMessage(&telego.SendMessageParams{ ChatID: tu.ID(h.ChatID), - Text: loc.Message("redmine_was_connected"), + Text: loc.Template("redmine_was_connected", map[string]interface{}{"Name": h.App.TGProfile().Username}), ParseMode: telego.ModeMarkdown, }) _, err = h.App.TG().SendMessage(&telego.SendMessageParams{ @@ -178,7 +181,7 @@ func (h *RedmineSetup) handleSPFieldStep(text string, loc locale.Localizer) erro ParseMode: telego.ModeMarkdown, }) store.RedmineSetups.Delete(h.ChatID) - return util.SendSetupDone(h.App.TG(), h.ChatID, loc) + return util.SendSetupDone(h.App.TG(), h.App.TGProfile().Username, h.ChatID, loc) } func (h *RedmineSetup) processURL(input string) string { diff --git a/internal/handler/wizard/register.go b/internal/handler/wizard/register.go index 735e1c3..d6d4f2a 100644 --- a/internal/handler/wizard/register.go +++ b/internal/handler/wizard/register.go @@ -16,6 +16,10 @@ func NewRegister(app iface.App, userID, chatID int64) *Register { } func (h *Register) Handle(wh telego.Update) error { + if wh.Message.Chat.Type != telego.ChatTypePrivate { + return nil + } + loc := h.Localizer(wh.Message.From.LanguageCode) userRepo := h.App.DB().ForUser() user, err := userRepo.ByTelegramID(wh.Message.From.ID) diff --git a/internal/locale/app.en.yaml b/internal/locale/app.en.yaml index 032161c..a48f8d5 100644 --- a/internal/locale/app.en.yaml +++ b/internal/locale/app.en.yaml @@ -1,5 +1,5 @@ welcome: "👋 Hello! This bot allows you to conduct Scrum Poker directly in a Telegram chat and pass results to issues tied to a Redmine instance. To start, add the bot to the chat where you want to conduct poker. After this, you can continue with the setup." -you_should_register_first: "You must register in the @vegapokerbot first before adding bot to the group." +you_should_register_first: "You must register in the @{{.Name}} first before adding bot to the group." too_many_members_in_the_group: "Bot doesn't support groups with more than {{.Limit}} members." internal_error: "❌ Internal error, try again later." bot_was_added: "Great, the bot has been added to the group *{{.Name}}*." @@ -8,7 +8,7 @@ standard_vote_keyboard: "⌨️ Standard poker keyboard" sp_vote_keyboard: "⌨️ Story Points poker keyboard" bot_was_removed_from_group: "⚠️ Bot was removed from group *{{.Name}}*. Group parameters have been deleted. You can restore connection with the group by adding the bot to the group and performing initial setup again." unknown_command: "❔ Unknown command. Please use /help to check the list of available commands." -help_output: "📄 Available commands:\n\n• /start - registers you in the bot;\n• /poll@vegapokerbot - starts the poker in group, works only in the connected groups;\n• /help - prints this message." +help_output: "📄 Available commands:\n\n• /start - registers you in the bot;\n• /poll@{{.Name}} - starts the poker in group, works only in the connected groups;\n• /help - prints this message." choose_at_least_one: "❌ You must choose at least one participant." ask_for_redmine: "📕 Configure integration with Redmine?" redmine_will_not_be_configured: "👌 Redmine will not be configured. You won't be able to send poker results to Redmine or load additional task information." @@ -21,11 +21,11 @@ use_this_command_in_group: "😒 This command should only be used in your group. please_send_redmine_url: "Please send the URL of your Redmine instance." please_send_redmine_key: "Please send your API key that will be used by the bot to interact with Redmine. The bot will perform the following actions:\n\n• retrieving task data;\n• writing the result of the poker in the task (if configured in subsequent steps).\n\nThe access key for the API can be found on the left part of the page by [this link]({{.Origin}}/my/account)." invalid_redmine_credentials: "⚠️ Failed to verify connection with Redmine. Will try setting up again?" -redmine_was_connected: "✔️ Great, Redmine is now connected! Now when starting a vote and passing it on to /poll@vegapokerbot in the chat, the bot will mention the task theme and generate a link to the task.\n\nThe bot can pass results of the vote directly into custom fields of issues in Redmine. The following data are passed:\n\n• the vote result;\n• the vote result converted to hours (assuming that the vote result is story points, conversion to hours is automatic)." +redmine_was_connected: "✔️ Great, Redmine is now connected! Now when starting a vote and passing it on to /poll@{{.Name}} in the chat, the bot will mention the task theme and generate a link to the task.\n\nThe bot can pass results of the vote directly into custom fields of issues in Redmine. The following data are passed:\n\n• the vote result;\n• the vote result converted to hours (assuming that the vote result is story points, conversion to hours is automatic)." should_send_poker_to_redmine: "🕑 Do you want to send poker results to Redmine? You will need to specify custom field name which will be used to send poker results." specify_result_field: "Please specify the name of the field where the poker result will be written." should_send_estimated_hours_redmine: "🕑 Do you want the poker result to be converted to hours and also sent to Redmine?\n\nBot will assume that you treat the vote result as story points. In that case the result will be multiplied by 8 before sending it to Redmine. Example: vote result 1.5 means that we will send to Redmine 1.5 ✖️ 8 = 12." -setup_done: "✔️ Done, now your chat is connected to the bot!\nUse the command /poll@vegapokerbot in the connected chat to start a vote." +setup_done: "✔️ Done, now your chat is connected to the bot!\nUse the command /poll@{{.Name}} in the connected chat to start a vote." yes: "✔️ Yes" no: "✖️ No" poker_start: "@{{.Name}} started poker{{.DotOrColon}}{{.Subject}}" diff --git a/internal/locale/app.ru.yaml b/internal/locale/app.ru.yaml index eea8c1b..279e49d 100644 --- a/internal/locale/app.ru.yaml +++ b/internal/locale/app.ru.yaml @@ -1,5 +1,5 @@ welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку." -you_should_register_first: "Перед добавлением бота в группу необходимо зарегистрироваться в @vegapokerbot." +you_should_register_first: "Перед добавлением бота в группу необходимо зарегистрироваться в @{{.Name}}." too_many_members_in_the_group: "Бот не поддерживает группы, в которых количество участников превышает {{.Limit}}." internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже." bot_was_added: "Отлично, бот был добавлен в группу *{{.Name}}*." @@ -8,7 +8,7 @@ standard_vote_keyboard: "⌨️ Стандартная poker-клавиатур sp_vote_keyboard: "⌨️ Story Points poker-клавиатура" bot_was_removed_from_group: "⚠️ Бот был удален из группы *{{.Name}}*. Настройки группы были удалены. Для того, чтобы восстановить связь с группой добавьте бота в нее и произведите настройку повторно." unknown_command: "❔ Неизвестная команда. Используйте /help для вывода списка доступных команд." -help_output: "📄 Доступные команды:\n\n• /start - регистрация в боте;\n• /poll@vegapokerbot - запускает poker в группе, работает только в подключенных группах;\n• /help - выводит эту справку." +help_output: "📄 Доступные команды:\n\n• /start - регистрация в боте;\n• /poll@{{.Name}} - запускает poker в группе, работает только в подключенных группах;\n• /help - выводит эту справку." choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника." ask_for_redmine: "📕 Настроить интеграцию с Redmine?" send_result_redmine: "📕 Отправить результат в Redmine" @@ -22,11 +22,11 @@ use_this_command_in_group: "😒 Эта команда должна исполь please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine." please_send_redmine_key: "Отправьте свой API-ключ, который будет использоваться ботом для взаимодействия с Redmine. Бот будет выполнять следующие действия:\n\n• получение данных задачи;\n• запись результата покера в задачу (если будет настроено в следующих шагах).\n\nКлюч доступа к API можно найти в левой части страницы по [этой ссылке]({{.Origin}}/my/account)." invalid_redmine_credentials: "⚠️ Не удалось проверить связь с Redmine. Будете пробовать настроить заново?" -redmine_was_connected: "✔️ Отлично, теперь Redmine подключен! Теперь при начале голосования и передаче в команду /poll@vegapokerbot номера задачи бот укажет в сообщении тему задачи и сгенерирует ссылку на задачу.\n\nБот может передавать результаты голосования напрямую в поля задачи в Redmine. Передаются следующие данные:\n\n• полученная в итоге голосования оценка;\n• конвертированная в часы оценка (если оценка в story points, конвертация в часы автоматическая)." +redmine_was_connected: "✔️ Отлично, теперь Redmine подключен! Теперь при начале голосования и передаче в команду /poll@{{.Name}} номера задачи бот укажет в сообщении тему задачи и сгенерирует ссылку на задачу.\n\nБот может передавать результаты голосования напрямую в поля задачи в Redmine. Передаются следующие данные:\n\n• полученная в итоге голосования оценка;\n• конвертированная в часы оценка (если оценка в story points, конвертация в часы автоматическая)." should_send_poker_to_redmine: "🕑 Передавать в Redmine результат проведения poker без конвертации в часы? Потребуется указать название кастомного поля для передачи данных." specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки." should_send_estimated_hours_redmine: "🕑 Передавать в поле *Оценка временных затрат* сконвертированный в часы результат оценки?\n\nПредполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 ✖️ 8 = 12." -setup_done: "✔️ Готово, чат подключен к боту!\nИспользуйте команду /poll@vegapokerbot в подключенном чате для запуска голосования." +setup_done: "✔️ Готово, чат подключен к боту!\nИспользуйте команду /poll@{{.Name}} в подключенном чате для запуска голосования." yes: "✔️ Да" no: "✖️ Нет" poker_start: "@{{.Name}} запустил покер{{.DotOrColon}}{{.Subject}}"