unhardcode bot username, ci/cd

This commit is contained in:
Pavel 2024-05-10 12:10:25 +03:00
parent e0ba5347a1
commit 86ba26f559
14 changed files with 115 additions and 32 deletions

20
.drone.yml Normal file
View File

@ -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

18
Dockerfile Normal file
View File

@ -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"]

View File

@ -5,12 +5,19 @@ ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
BIN=$(ROOT_DIR)/build/vegapokerbot BIN=$(ROOT_DIR)/build/vegapokerbot
GO_VERSION=$(shell go version | sed -e 's/go version //') GO_VERSION=$(shell go version | sed -e 's/go version //')
BIN_DIR=$(ROOT_DIR)/build BIN_DIR=$(ROOT_DIR)/build
APP_IMAGE=gitea.neur0tx.site/neur0toxine/vegapokerbot:latest
build: deps fmt build: deps fmt
@echo "> building with ${GO_VERSION}" @echo "> building with ${GO_VERSION}"
@CGO_ENABLED=0 go build -buildvcs=false -tags=release -o $(BIN) . @CGO_ENABLED=0 go build -buildvcs=false -tags=release -o $(BIN) .
@echo $(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: run:
@${BIN} run @${BIN} run
fmt: fmt:

27
compose.prod.yml Normal file
View File

@ -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

View File

@ -18,9 +18,7 @@ type Config struct {
func LoadConfig() (*Config, error) { func LoadConfig() (*Config, error) {
var cfg Config var cfg Config
if err := godotenv.Load(); err != nil { _ = godotenv.Load()
return nil, err
}
loader := aconfig.LoaderFor(&cfg, aconfig.Config{ loader := aconfig.LoaderFor(&cfg, aconfig.Config{
SkipFlags: true, SkipFlags: true,
EnvPrefix: "POKER_BOT", EnvPrefix: "POKER_BOT",

View File

@ -367,7 +367,7 @@ func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, chatID in
MessageID: msgID, MessageID: msgID,
Text: loc.Message("redmine_will_not_be_configured"), 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{ _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
ChatID: tu.ID(user.ChatID), ChatID: tu.ID(user.ChatID),
@ -428,7 +428,7 @@ func (h *CallbackQueryHandler) handleAnswerRedmineSendResults(answer util.Answer
MessageID: msgID, MessageID: msgID,
Text: loc.Message("redmine_poker_will_not_be_configured"), 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{ _, err := h.App.TG().EditMessageText(&telego.EditMessageTextParams{
ChatID: tu.ID(user.ChatID), ChatID: tu.ID(user.ChatID),

View File

@ -53,7 +53,8 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
if user == nil || user.ID == 0 { if user == nil || user.ID == 0 {
_, err = h.App.TG().SendMessage(&telego.SendMessageParams{ _, err = h.App.TG().SendMessage(&telego.SendMessageParams{
ChatID: tu.ID(tgChat.ID), ChatID: tu.ID(tgChat.ID),
Text: h.Localizer(language.English.String()).Message("you_should_register_first"), Text: h.Localizer(language.English.String()).
Template("you_should_register_first", map[string]interface{}{"Name": h.App.TGProfile().Username}),
ParseMode: telego.ModeMarkdown, ParseMode: telego.ModeMarkdown,
}) })
h.leaveChat(tgChat.ID) h.leaveChat(tgChat.ID)

View File

@ -21,23 +21,27 @@ func (h *MessageHandler) Handle(wh telego.Update) error {
if wh.Message.From == nil { if wh.Message.From == nil {
return nil return nil
} }
if wh.Message.Chat.Type == telego.ChatTypePrivate {
if util.MatchCommand("start", wh.Message) { if util.MatchCommand("start", wh.Message) {
return wizard.NewRegister(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) 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("poll", wh.Message) { if util.MatchCommand("poll", wh.Message) {
return group.NewPoll(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) 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) return wizard.NewUnknownCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
} }

View File

@ -6,10 +6,10 @@ import (
tu "github.com/mymmrac/telego/telegoutil" 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{ _, err := tg.SendMessage(&telego.SendMessageParams{
ChatID: tu.ID(chatID), ChatID: tu.ID(chatID),
Text: loc.Message("setup_done"), Text: loc.Template("setup_done", map[string]interface{}{"Name": username}),
}) })
return err return err
} }

View File

@ -17,7 +17,8 @@ func NewHelpCommand(app iface.App, userID, chatID int64) *HelpCommand {
func (h *HelpCommand) Handle(wh telego.Update) error { func (h *HelpCommand) Handle(wh telego.Update) error {
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{ _, err := h.App.TG().SendMessage(&telego.SendMessageParams{
ChatID: tu.ID(wh.Message.Chat.ID), ChatID: tu.ID(wh.Message.Chat.ID),
Text: h.Localizer(wh.Message.From.LanguageCode).Message("help_output"), Text: h.Localizer(wh.Message.From.LanguageCode).
Template("help_output", map[string]interface{}{"Name": h.App.TGProfile().Username}),
ParseMode: telego.ModeMarkdown, ParseMode: telego.ModeMarkdown,
}) })
return err return err

View File

@ -27,6 +27,9 @@ func NewRedmineSetup(app iface.App, userID, chatID int64, config *store.RedmineS
} }
func (h *RedmineSetup) Handle(wh telego.Update) error { func (h *RedmineSetup) Handle(wh telego.Update) error {
if wh.Message.Chat.Type != telego.ChatTypePrivate {
return nil
}
if h.Redmine == nil { if h.Redmine == nil {
return nil return nil
} }
@ -119,7 +122,7 @@ func (h *RedmineSetup) handleKeyStep(text string, loc locale.Localizer) error {
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{ _, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
ChatID: tu.ID(h.ChatID), 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, ParseMode: telego.ModeMarkdown,
}) })
_, err = h.App.TG().SendMessage(&telego.SendMessageParams{ _, err = h.App.TG().SendMessage(&telego.SendMessageParams{
@ -178,7 +181,7 @@ func (h *RedmineSetup) handleSPFieldStep(text string, loc locale.Localizer) erro
ParseMode: telego.ModeMarkdown, ParseMode: telego.ModeMarkdown,
}) })
store.RedmineSetups.Delete(h.ChatID) 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 { func (h *RedmineSetup) processURL(input string) string {

View File

@ -16,6 +16,10 @@ func NewRegister(app iface.App, userID, chatID int64) *Register {
} }
func (h *Register) Handle(wh telego.Update) error { func (h *Register) Handle(wh telego.Update) error {
if wh.Message.Chat.Type != telego.ChatTypePrivate {
return nil
}
loc := h.Localizer(wh.Message.From.LanguageCode) loc := h.Localizer(wh.Message.From.LanguageCode)
userRepo := h.App.DB().ForUser() userRepo := h.App.DB().ForUser()
user, err := userRepo.ByTelegramID(wh.Message.From.ID) user, err := userRepo.ByTelegramID(wh.Message.From.ID)

View File

@ -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." 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." too_many_members_in_the_group: "Bot doesn't support groups with more than {{.Limit}} members."
internal_error: "❌ Internal error, try again later." internal_error: "❌ Internal error, try again later."
bot_was_added: "Great, the bot has been added to the group *{{.Name}}*." 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" 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." 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." 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 <task> - 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}} <task> - 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." choose_at_least_one: "❌ You must choose at least one participant."
ask_for_redmine: "📕 Configure integration with Redmine?" 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." 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_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)." 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?" 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." 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." 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." 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" yes: "✔️ Yes"
no: "✖️ No" no: "✖️ No"
poker_start: "@{{.Name}} started poker{{.DotOrColon}}{{.Subject}}" poker_start: "@{{.Name}} started poker{{.DotOrColon}}{{.Subject}}"

View File

@ -1,5 +1,5 @@
welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку." welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
you_should_register_first: "Перед добавлением бота в группу необходимо зарегистрироваться в @vegapokerbot." you_should_register_first: "Перед добавлением бота в группу необходимо зарегистрироваться в @{{.Name}}."
too_many_members_in_the_group: "Бот не поддерживает группы, в которых количество участников превышает {{.Limit}}." too_many_members_in_the_group: "Бот не поддерживает группы, в которых количество участников превышает {{.Limit}}."
internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже." internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже."
bot_was_added: "Отлично, бот был добавлен в группу *{{.Name}}*." bot_was_added: "Отлично, бот был добавлен в группу *{{.Name}}*."
@ -8,7 +8,7 @@ standard_vote_keyboard: "⌨️ Стандартная poker-клавиатур
sp_vote_keyboard: "⌨️ Story Points poker-клавиатура" sp_vote_keyboard: "⌨️ Story Points poker-клавиатура"
bot_was_removed_from_group: "⚠️ Бот был удален из группы *{{.Name}}*. Настройки группы были удалены. Для того, чтобы восстановить связь с группой добавьте бота в нее и произведите настройку повторно." bot_was_removed_from_group: "⚠️ Бот был удален из группы *{{.Name}}*. Настройки группы были удалены. Для того, чтобы восстановить связь с группой добавьте бота в нее и произведите настройку повторно."
unknown_command: "❔ Неизвестная команда. Используйте /help для вывода списка доступных команд." unknown_command: "❔ Неизвестная команда. Используйте /help для вывода списка доступных команд."
help_output: "📄 Доступные команды:\n\n• /start - регистрация в боте;\n• /poll@vegapokerbot <task> - запускает poker в группе, работает только в подключенных группах;\n• /help - выводит эту справку." help_output: "📄 Доступные команды:\n\n• /start - регистрация в боте;\n• /poll@{{.Name}} <task> - запускает poker в группе, работает только в подключенных группах;\n• /help - выводит эту справку."
choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника." choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника."
ask_for_redmine: "📕 Настроить интеграцию с Redmine?" ask_for_redmine: "📕 Настроить интеграцию с Redmine?"
send_result_redmine: "📕 Отправить результат в Redmine" send_result_redmine: "📕 Отправить результат в Redmine"
@ -22,11 +22,11 @@ use_this_command_in_group: "😒 Эта команда должна исполь
please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine." please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine."
please_send_redmine_key: "Отправьте свой API-ключ, который будет использоваться ботом для взаимодействия с Redmine. Бот будет выполнять следующие действия:\n\n• получение данных задачи;\n• запись результата покера в задачу (если будет настроено в следующих шагах).\n\nКлюч доступа к API можно найти в левой части страницы по [этой ссылке]({{.Origin}}/my/account)." please_send_redmine_key: "Отправьте свой API-ключ, который будет использоваться ботом для взаимодействия с Redmine. Бот будет выполнять следующие действия:\n\n• получение данных задачи;\n• запись результата покера в задачу (если будет настроено в следующих шагах).\n\nКлюч доступа к API можно найти в левой части страницы по [этой ссылке]({{.Origin}}/my/account)."
invalid_redmine_credentials: "⚠️ Не удалось проверить связь с Redmine. Будете пробовать настроить заново?" 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 без конвертации в часы? Потребуется указать название кастомного поля для передачи данных." should_send_poker_to_redmine: "🕑 Передавать в Redmine результат проведения poker без конвертации в часы? Потребуется указать название кастомного поля для передачи данных."
specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки." specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки."
should_send_estimated_hours_redmine: "🕑 Передавать в поле *Оценка временных затрат* сконвертированный в часы результат оценки?\n\nПредполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 ✖️ 8 = 12." 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: "✔️ Да" yes: "✔️ Да"
no: "✖️ Нет" no: "✖️ Нет"
poker_start: "@{{.Name}} запустил покер{{.DotOrColon}}{{.Subject}}" poker_start: "@{{.Name}} запустил покер{{.DotOrColon}}{{.Subject}}"