done: group setup, redmine setup
This commit is contained in:
parent
d7b175fcd1
commit
d615a04e61
@ -3,16 +3,51 @@ package model
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type Integration struct {
|
||||
ID uint64 `gorm:"primaryKey; autoIncrement;"`
|
||||
Type IntegrationType `gorm:"column:type; not null"`
|
||||
ChatID int64 `gorm:"column:chat_id; not null"`
|
||||
ChatID uint64 `gorm:"column:chat_id; not null"`
|
||||
Params datatypes.JSONMap `gorm:"column:params; not null"`
|
||||
}
|
||||
|
||||
func (i *Integration) StoreRedmine(rs *store.RedmineSetup) {
|
||||
i.Params = map[string]interface{}{
|
||||
"url": rs.URL,
|
||||
"key": rs.Key,
|
||||
"save_hours": rs.SaveHours,
|
||||
"sp_field": rs.SPFieldName,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Integration) LoadRedmine() *store.RedmineSetup {
|
||||
rs := &store.RedmineSetup{}
|
||||
if param, ok := i.Params["url"]; ok {
|
||||
if val, ok := param.(string); ok {
|
||||
rs.URL = val
|
||||
}
|
||||
}
|
||||
if param, ok := i.Params["key"]; ok {
|
||||
if val, ok := param.(string); ok {
|
||||
rs.Key = val
|
||||
}
|
||||
}
|
||||
if param, ok := i.Params["save_hours"]; ok {
|
||||
if val, ok := param.(bool); ok {
|
||||
rs.SaveHours = val
|
||||
}
|
||||
}
|
||||
if param, ok := i.Params["sp_field"]; ok {
|
||||
if val, ok := param.(string); ok {
|
||||
rs.SPFieldName = val
|
||||
}
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
func (Integration) TableName() string {
|
||||
return "integration"
|
||||
}
|
||||
@ -35,6 +70,14 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func (it IntegrationType) String() string {
|
||||
val, ok := idsToIntegrationTypes[it]
|
||||
if ok {
|
||||
return val
|
||||
}
|
||||
return idsToIntegrationTypes[InvalidIntegration]
|
||||
}
|
||||
|
||||
func (it *IntegrationType) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
|
@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/util"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -13,6 +14,14 @@ func NewIntegration(db *gorm.DB) *Integration {
|
||||
return &Integration{db: db}
|
||||
}
|
||||
|
||||
func (r *Integration) LoadForChatAndType(chatID uint64, typ model.IntegrationType) (*model.Integration, error) {
|
||||
var integration model.Integration
|
||||
if err := r.db.Model(model.Integration{ChatID: chatID, Type: typ}).First(&integration).Error; err != nil {
|
||||
return nil, util.HandleRecordNotFound(err)
|
||||
}
|
||||
return &integration, nil
|
||||
}
|
||||
|
||||
func (r *Integration) Save(integration *model.Integration) error {
|
||||
return r.db.Model(integration).Save(integration).Error
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"github.com/mymmrac/telego"
|
||||
@ -43,9 +44,11 @@ func (h *CallbackQueryHandler) handleSetupKey(cq *telego.CallbackQuery, user *mo
|
||||
answer := pl.YesNoAnswer()
|
||||
switch answer.Type {
|
||||
case util.QuestionTypeRedmine:
|
||||
return h.handleAnswerRedmine(answer, cq.Message.GetMessageID(), user)
|
||||
return h.handleAnswerRedmine(answer, pl.Chat, cq.Message.GetMessageID(), user)
|
||||
case util.QuestionTypeRedmineHours:
|
||||
return h.handleAnswerRedmineHours(answer, pl.Chat, cq.Message.GetMessageID(), user)
|
||||
case util.QuestionTypeRedmineSendResult:
|
||||
return h.handleAnswerRedmineSendResults(answer, cq.Message.GetMessageID(), user)
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
@ -66,13 +69,13 @@ func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int,
|
||||
if chat == nil || chat.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
chat.KeyboardType = result.Type
|
||||
chat.KeyboardType = model.KeyboardType(result.Type)
|
||||
if err := cr.Save(chat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kbTypeName := "standard_vote_keyboard"
|
||||
if result.Type == model.StoryPointsKeyboard {
|
||||
if model.KeyboardType(result.Type) == model.StoryPointsKeyboard {
|
||||
kbTypeName = "sp_vote_keyboard"
|
||||
}
|
||||
loc := h.app.Localizer(user.Language)
|
||||
@ -101,7 +104,7 @@ func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int,
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, msgID int, user *model.User) error {
|
||||
func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, chatID int64, msgID int, user *model.User) error {
|
||||
loc := h.app.Localizer(user.Language)
|
||||
if !answer.Result {
|
||||
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
@ -109,43 +112,103 @@ func (h *CallbackQueryHandler) handleAnswerRedmine(answer util.Answer, msgID int
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("redmine_will_not_be_configured"),
|
||||
})
|
||||
return sendSetupDoneMessage(h.app.TG(), user.ChatID, loc)
|
||||
return util.SendSetupDone(h.app.TG(), user.ChatID, loc)
|
||||
}
|
||||
// @todo Ask for Redmine URL and proceed with asking for Redmine key in NewMessageHandler. Save both to Integration.
|
||||
return nil
|
||||
_, err := h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("please_send_redmine_url"),
|
||||
})
|
||||
store.RedmineSetups.Set(user.ChatID, &store.RedmineSetup{Chat: chatID})
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleAnswerRedmineHours(answer util.Answer, chatID int64, msgID int, user *model.User) error {
|
||||
loc := h.app.Localizer(user.Language)
|
||||
message := "redmine_hours_will_not_be_configured"
|
||||
if answer.Result {
|
||||
message = "redmine_hours_will_be_configured"
|
||||
}
|
||||
|
||||
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message(message),
|
||||
})
|
||||
|
||||
rs, found := store.RedmineSetups.Get(user.ChatID)
|
||||
if found {
|
||||
rs.SaveHours = answer.Result
|
||||
if err := h.updateRedmineIntegration(rs, loc, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
Text: loc.Message("should_send_poker_to_redmine"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
ReplyMarkup: &telego.InlineKeyboardMarkup{InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: loc.Message("yes"),
|
||||
CallbackData: util.NewRedmineSendResultQuestionPayload(user.TelegramID, chatID, true).String(),
|
||||
},
|
||||
{
|
||||
Text: loc.Message("no"),
|
||||
CallbackData: util.NewRedmineSendResultQuestionPayload(user.TelegramID, chatID, false).String(),
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) handleAnswerRedmineSendResults(answer util.Answer, msgID int, user *model.User) error {
|
||||
loc := h.app.Localizer(user.Language)
|
||||
if !answer.Result {
|
||||
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("redmine_hours_will_not_be_configured"),
|
||||
Text: loc.Message("redmine_poker_will_not_be_configured"),
|
||||
})
|
||||
return sendSetupDoneMessage(h.app.TG(), user.ChatID, loc)
|
||||
return util.SendSetupDone(h.app.TG(), user.ChatID, loc)
|
||||
}
|
||||
cr := h.app.DB().ForChat()
|
||||
chat, err := cr.ByTelegramIDWithIntegrations(chatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, it := range chat.Integrations {
|
||||
if it.Type == model.RedmineIntegration {
|
||||
it.Params["save_hours"] = true
|
||||
if err := h.app.DB().ForIntegration().Save(&it); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return sendSetupDoneMessage(h.app.TG(), user.ChatID, loc)
|
||||
}
|
||||
|
||||
func sendSetupDoneMessage(tg *telego.Bot, chatID int64, loc locale.Localizer) error {
|
||||
_, err := tg.SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
Text: loc.Message("setup_done"),
|
||||
_, err := h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
MessageID: msgID,
|
||||
Text: loc.Message("specify_result_field"),
|
||||
})
|
||||
rs, _ := store.RedmineSetups.Get(user.ChatID)
|
||||
rs.WaitingForSPField = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CallbackQueryHandler) updateRedmineIntegration(rs *store.RedmineSetup, loc locale.Localizer, user *model.User) error {
|
||||
dbChat, err := h.app.DB().ForChat().ByTelegramID(rs.Chat)
|
||||
if dbChat == nil || dbChat.ID == 0 {
|
||||
_ = util.SendInternalError(h.app.TG(), user.ChatID, loc)
|
||||
return err
|
||||
}
|
||||
savedRedmine, err := h.app.DB().ForIntegration().LoadForChatAndType(dbChat.ID, model.RedmineIntegration)
|
||||
if savedRedmine == nil || savedRedmine.ID == 0 {
|
||||
_ = util.SendInternalError(h.app.TG(), user.ChatID, loc)
|
||||
return err
|
||||
}
|
||||
var shouldUpdate bool
|
||||
savedRS := savedRedmine.LoadRedmine()
|
||||
if savedRS.SaveHours != rs.SaveHours {
|
||||
savedRS.SaveHours = rs.SaveHours
|
||||
shouldUpdate = true
|
||||
}
|
||||
if savedRS.SPFieldName != rs.SPFieldName {
|
||||
savedRS.SPFieldName = rs.SPFieldName
|
||||
shouldUpdate = true
|
||||
}
|
||||
if shouldUpdate {
|
||||
savedRedmine.StoreRedmine(savedRS)
|
||||
return h.app.DB().ForIntegration().Save(savedRedmine)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -39,14 +39,14 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
cr := h.app.DB().ForChat()
|
||||
chat, err := cr.ByTelegramID(tgChat.ID)
|
||||
if err != nil {
|
||||
h.sendInternalError(tgChat.ID, nil)
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, nil)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := h.getRegisteredAdmin(tgChat.ID)
|
||||
if err != nil {
|
||||
h.sendInternalError(tgChat.ID, nil)
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, nil)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
@ -66,7 +66,7 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
ChatID: tu.ID(tgChat.ID),
|
||||
})
|
||||
if err != nil {
|
||||
h.sendInternalError(tgChat.ID, nil)
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, nil)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
@ -87,7 +87,7 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
}
|
||||
err := cr.Save(chat)
|
||||
if err != nil {
|
||||
h.sendInternalError(tgChat.ID, loc)
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, loc)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
@ -95,13 +95,13 @@ func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
chat.UserID = user.ID
|
||||
err := h.app.DB().ForIntegration().DeleteForChat(chat.ID)
|
||||
if err != nil {
|
||||
h.sendInternalError(tgChat.ID, loc)
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, loc)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
err = cr.Save(chat)
|
||||
if err != nil {
|
||||
h.sendInternalError(tgChat.ID, loc)
|
||||
_ = util.SendInternalError(h.app.TG(), tgChat.ID, loc)
|
||||
h.leaveChat(tgChat.ID)
|
||||
return err
|
||||
}
|
||||
@ -129,6 +129,7 @@ func (h *ChatMemberUpdatedHandler) handleRemoveFromChat(tgChat telego.Chat) erro
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
Text: h.app.Localizer(user.Language).Template(
|
||||
"bot_was_removed_from_group", map[string]interface{}{"Name": tgChat.Title}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
}
|
||||
return cr.Delete(chat)
|
||||
@ -147,14 +148,14 @@ func (h *ChatMemberUpdatedHandler) sendKeyboardChooser(chat telego.Chat, user *m
|
||||
{
|
||||
Text: loc.Message("standard_vote_keyboard"),
|
||||
CallbackData: util.NewKeyboardChooserPayload(
|
||||
user.TelegramID, chat.ID, model.StandardKeyboard).String(),
|
||||
user.TelegramID, chat.ID, uint8(model.StandardKeyboard)).String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
{
|
||||
Text: loc.Message("sp_vote_keyboard"),
|
||||
CallbackData: util.NewKeyboardChooserPayload(
|
||||
user.TelegramID, chat.ID, model.StoryPointsKeyboard).String(),
|
||||
user.TelegramID, chat.ID, uint8(model.StoryPointsKeyboard)).String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -189,14 +190,3 @@ func (h *ChatMemberUpdatedHandler) leaveChat(chatID int64) {
|
||||
ChatID: tu.ID(chatID),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) sendInternalError(chatID int64, loc locale.Localizer) {
|
||||
if loc == nil {
|
||||
loc = h.app.Localizer(language.English.String())
|
||||
}
|
||||
_, _ = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
Text: loc.Message("internal_error"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package iface
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
@ -15,6 +16,7 @@ func NewBase(app App, userID int64, chatID int64) Base {
|
||||
}
|
||||
|
||||
func (b Base) Localizer(lang string) locale.Localizer {
|
||||
lang = strings.ToLower(lang)
|
||||
if len(lang) > 2 {
|
||||
lang = lang[:2]
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ type ChatRepository interface {
|
||||
}
|
||||
|
||||
type IntegrationRepository interface {
|
||||
LoadForChatAndType(chatID uint64, typ model.IntegrationType) (*model.Integration, error)
|
||||
Save(integration *model.Integration) error
|
||||
DeleteForChat(id uint64) error
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/wizard"
|
||||
"github.com/mymmrac/telego"
|
||||
@ -25,6 +26,12 @@ func (h *MessageHandler) Handle(wh telego.Update) error {
|
||||
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)
|
||||
}
|
||||
|
||||
return wizard.NewUnknownCommand(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
||||
|
30
internal/handler/store/init.go
Normal file
30
internal/handler/store/init.go
Normal file
@ -0,0 +1,30 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/maypok86/otter"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cache, err := otter.MustBuilder[int64, PollState](10_000).
|
||||
Cost(func(key int64, value PollState) uint32 {
|
||||
return 1
|
||||
}).
|
||||
WithTTL(time.Hour * 24 * 7).
|
||||
Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Polls = cache
|
||||
|
||||
redmineSetups, err := otter.MustBuilder[int64, *RedmineSetup](1000).
|
||||
Cost(func(key int64, value *RedmineSetup) uint32 {
|
||||
return 1
|
||||
}).
|
||||
WithTTL(time.Hour * 24 * 7).
|
||||
Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
RedmineSetups = redmineSetups
|
||||
}
|
@ -2,8 +2,6 @@ package store
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"time"
|
||||
|
||||
"github.com/maypok86/otter"
|
||||
)
|
||||
|
||||
@ -26,16 +24,3 @@ type PollState struct {
|
||||
}
|
||||
|
||||
var Polls otter.Cache[int64, PollState]
|
||||
|
||||
func init() {
|
||||
cache, err := otter.MustBuilder[int64, PollState](10_000).
|
||||
Cost(func(key int64, value PollState) uint32 {
|
||||
return 1
|
||||
}).
|
||||
WithTTL(time.Hour * 24 * 7).
|
||||
Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Polls = cache
|
||||
}
|
||||
|
14
internal/handler/store/redmine_setups.go
Normal file
14
internal/handler/store/redmine_setups.go
Normal file
@ -0,0 +1,14 @@
|
||||
package store
|
||||
|
||||
import "github.com/maypok86/otter"
|
||||
|
||||
type RedmineSetup struct {
|
||||
Chat int64
|
||||
URL string
|
||||
Key string
|
||||
SaveHours bool
|
||||
SPFieldName string
|
||||
WaitingForSPField bool
|
||||
}
|
||||
|
||||
var RedmineSetups otter.Cache[int64, *RedmineSetup]
|
23
internal/handler/util/common_messages.go
Normal file
23
internal/handler/util/common_messages.go
Normal file
@ -0,0 +1,23 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
func SendSetupDone(tg *telego.Bot, chatID int64, loc locale.Localizer) error {
|
||||
_, err := tg.SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
Text: loc.Message("setup_done"),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func SendInternalError(tg *telego.Bot, chatID int64, loc locale.Localizer) error {
|
||||
_, err := tg.SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
Text: loc.Message("internal_error"),
|
||||
})
|
||||
return err
|
||||
}
|
@ -2,7 +2,6 @@ package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
)
|
||||
|
||||
type PayloadAction uint8
|
||||
@ -18,6 +17,7 @@ type QuestionType uint8
|
||||
const (
|
||||
QuestionTypeRedmine = iota
|
||||
QuestionTypeRedmineHours
|
||||
QuestionTypeRedmineSendResult
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
@ -62,7 +62,7 @@ type Member struct {
|
||||
}
|
||||
|
||||
type KBChooserData struct {
|
||||
Type model.KeyboardType `json:"k"`
|
||||
Type uint8 `json:"k"`
|
||||
}
|
||||
|
||||
type Vote struct {
|
||||
@ -74,7 +74,7 @@ type Answer struct {
|
||||
Result bool `json:"r"`
|
||||
}
|
||||
|
||||
func NewKeyboardChooserPayload(userID, chatID int64, kbType model.KeyboardType) *Payload {
|
||||
func NewKeyboardChooserPayload(userID, chatID int64, kbType uint8) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionChooseKeyboard,
|
||||
User: userID,
|
||||
@ -111,6 +111,15 @@ func NewRedmineHoursQuestionPayload(userID int64, chatID int64, isYes bool) *Pay
|
||||
}
|
||||
}
|
||||
|
||||
func NewRedmineSendResultQuestionPayload(userID int64, chatID int64, isYes bool) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionYesNo,
|
||||
User: userID,
|
||||
Chat: chatID,
|
||||
Data: marshal(Answer{Type: QuestionTypeRedmineSendResult, Result: isYes}),
|
||||
}
|
||||
}
|
||||
|
||||
func marshal(val interface{}) json.RawMessage {
|
||||
data, _ := json.Marshal(val)
|
||||
return data
|
||||
|
@ -7,17 +7,17 @@ import (
|
||||
)
|
||||
|
||||
type HelpCommand struct {
|
||||
UnknownCommand
|
||||
iface.Base
|
||||
}
|
||||
|
||||
func NewHelpCommand(app iface.App, userID, chatID int64) *HelpCommand {
|
||||
return &HelpCommand{*NewUnknownCommand(app, userID, chatID)}
|
||||
return &HelpCommand{iface.NewBase(app, userID, chatID)}
|
||||
}
|
||||
|
||||
func (h *HelpCommand) Handle(wh telego.Update) error {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: h.loc(wh.Message.From.LanguageCode).Message("help_output"),
|
||||
Text: h.Localizer(wh.Message.From.LanguageCode).Message("help_output"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
|
191
internal/handler/wizard/redmine_setup.go
Normal file
191
internal/handler/wizard/redmine_setup.go
Normal file
@ -0,0 +1,191 @@
|
||||
package wizard
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedmineSetup struct {
|
||||
iface.Base
|
||||
Redmine *store.RedmineSetup
|
||||
}
|
||||
|
||||
func NewRedmineSetup(app iface.App, userID, chatID int64, config *store.RedmineSetup) *RedmineSetup {
|
||||
return &RedmineSetup{
|
||||
Base: iface.NewBase(app, userID, chatID),
|
||||
Redmine: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RedmineSetup) Handle(wh telego.Update) error {
|
||||
if h.Redmine == nil {
|
||||
return nil
|
||||
}
|
||||
loc := h.Localizer(wh.Message.From.LanguageCode)
|
||||
if h.Redmine.URL == "" {
|
||||
h.Redmine.URL = h.processURL(wh.Message.Text)
|
||||
if h.Redmine.URL == "" {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Message("please_send_redmine_url"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Template("please_send_redmine_key", map[string]interface{}{"Origin": h.Redmine.URL}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if h.Redmine.Key == "" {
|
||||
h.Redmine.Key = strings.TrimSpace(wh.Message.Text)
|
||||
if h.Redmine.Key == "" {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Template("please_send_redmine_key", map[string]interface{}{"Origin": h.Redmine.URL}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if !h.verifyRedmine() {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Message("invalid_redmine_credentials"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
ReplyMarkup: &telego.InlineKeyboardMarkup{InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: loc.Message("yes"),
|
||||
CallbackData: util.NewRedmineQuestionPayload(h.UserID, h.Redmine.Chat, true).String(),
|
||||
},
|
||||
{
|
||||
Text: loc.Message("no"),
|
||||
CallbackData: util.NewRedmineQuestionPayload(h.UserID, h.Redmine.Chat, false).String(),
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
chat, err := h.App.DB().ForChat().ByTelegramID(h.Redmine.Chat)
|
||||
if err != nil {
|
||||
_ = util.SendInternalError(h.App.TG(), h.ChatID, loc)
|
||||
return err
|
||||
}
|
||||
if chat == nil || chat.ID == 0 {
|
||||
_ = util.SendInternalError(h.App.TG(), h.ChatID, loc)
|
||||
return nil
|
||||
}
|
||||
integration := &model.Integration{
|
||||
Type: model.RedmineIntegration,
|
||||
ChatID: chat.ID,
|
||||
}
|
||||
integration.StoreRedmine(h.Redmine)
|
||||
if err := h.App.DB().ForIntegration().Save(integration); err != nil {
|
||||
_ = util.SendInternalError(h.App.TG(), h.ChatID, loc)
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Message("redmine_was_connected"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
_, err = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Message("should_send_estimated_hours_redmine"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
ReplyMarkup: &telego.InlineKeyboardMarkup{InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: loc.Message("yes"),
|
||||
CallbackData: util.NewRedmineHoursQuestionPayload(h.UserID, h.Redmine.Chat, true).String(),
|
||||
},
|
||||
{
|
||||
Text: loc.Message("no"),
|
||||
CallbackData: util.NewRedmineHoursQuestionPayload(h.UserID, h.Redmine.Chat, false).String(),
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if h.Redmine.SPFieldName == "" && h.Redmine.WaitingForSPField {
|
||||
h.Redmine.SPFieldName = strings.TrimSpace(wh.Message.Text)
|
||||
if h.Redmine.SPFieldName == "" {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Message("specify_result_field"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
h.Redmine.WaitingForSPField = false
|
||||
chat, err := h.App.DB().ForChat().ByTelegramIDWithIntegrations(h.Redmine.Chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, integration := range chat.Integrations {
|
||||
if integration.Type == model.RedmineIntegration {
|
||||
rs := integration.LoadRedmine()
|
||||
rs.SPFieldName = h.Redmine.SPFieldName
|
||||
integration.StoreRedmine(rs)
|
||||
if err := h.App.DB().ForIntegration().Save(&integration); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(h.ChatID),
|
||||
Text: loc.Template("redmine_poker_will_be_configured",
|
||||
map[string]interface{}{"Name": h.Redmine.SPFieldName}),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
store.RedmineSetups.Delete(h.ChatID)
|
||||
return util.SendSetupDone(h.App.TG(), h.ChatID, loc)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RedmineSetup) processURL(input string) string {
|
||||
uri, err := url.Parse(strings.TrimSpace(input))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return uri.Scheme + "://" + uri.Host
|
||||
}
|
||||
|
||||
func (h *RedmineSetup) verifyRedmine() bool {
|
||||
req, err := http.NewRequest(http.MethodGet, h.Redmine.URL+"/issues.json", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
req.Header.Set("X-Redmine-Api-Key", h.Redmine.Key)
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
@ -2,11 +2,8 @@ package wizard
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
"golang.org/x/text/language"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UnknownCommand struct {
|
||||
@ -20,19 +17,8 @@ func NewUnknownCommand(app iface.App, userID, chatID int64) *UnknownCommand {
|
||||
func (h *UnknownCommand) Handle(wh telego.Update) error {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: h.loc(wh.Message.From.LanguageCode).Message("unknown_command"),
|
||||
Text: h.Localizer(wh.Message.From.LanguageCode).Message("unknown_command"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *UnknownCommand) loc(loc string) locale.Localizer {
|
||||
loc = strings.ToLower(loc)
|
||||
if len(loc) > 2 {
|
||||
loc = loc[:2]
|
||||
}
|
||||
if loc != "en" && loc != "ru" {
|
||||
return h.App.Localizer(language.English.String())
|
||||
}
|
||||
return h.App.Localizer(loc)
|
||||
}
|
||||
|
@ -2,25 +2,28 @@ welcome: "👋 Hello! This bot allows you to conduct Scrum Poker directly in a T
|
||||
you_should_register_first: "You must register in the @vegapokerbot 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}}\"."
|
||||
choose_keyboard: "⌨️ Choose a keyboard for poker in the {{.Name}} group. There are two types of keyboards available:\n\n• Standard - for regular poker, it uses Fibonacci numbers up to 13;\n• Story Points Poker Keyboard - a keyboard with buttons from 0.5 to 10 with step 0.5."
|
||||
bot_was_added: "Great, the bot has been added to the group *{{.Name}}*."
|
||||
choose_keyboard: "⌨️ Choose a keyboard for poker in the *{{.Name}}* group. There are two types of keyboards available:\n\n• Standard - for regular poker, it uses Fibonacci numbers up to 13;\n• Story Points Poker Keyboard - a keyboard with buttons from 0.5 to 10 with step 0.5."
|
||||
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."
|
||||
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 <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."
|
||||
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_hours_will_not_be_configured: "Vote results won't be sent to Redmine as hours."
|
||||
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_hours_will_not_be_configured: "👌 Vote results won't be sent to Redmine as hours."
|
||||
redmine_hours_will_be_configured: "👌 Vote results will be be sent to Redmine as hours."
|
||||
redmine_poker_will_not_be_configured: "👌 Vote results won't be sent to Redmine."
|
||||
redmine_poker_will_be_configured: "✔️ Done, vote results will be sent to Redmine in the field *{{.Name}}*."
|
||||
chosen_keyboard: "{{.Name}} has been selected."
|
||||
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](/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?"
|
||||
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).\n\nDo you want to send this data to Redmine at the end of the poker?"
|
||||
specify_result_field: "Specify the name of the field where the poker result will be written."
|
||||
should_also_send_hours: "Do you want the poker result to be converted to hours and also sent to Redmine? This is useful if 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."
|
||||
specify_second_result_field: "Specify the name of the field where the poker result in hours will be writen."
|
||||
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)."
|
||||
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."
|
||||
yes: "✔️ Yes"
|
||||
no: "✖️ No"
|
||||
|
@ -2,24 +2,28 @@ welcome: "👋 Привет! Этот бот позволяет проводит
|
||||
you_should_register_first: "Перед добавлением бота в группу необходимо зарегистрироваться в @vegapokerbot."
|
||||
too_many_members_in_the_group: "Бот не поддерживает группы, в которых количество участников превышает {{.Limit}}."
|
||||
internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже."
|
||||
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\"."
|
||||
choose_keyboard: "⌨️ Выберите клавиатуру для голосований в группе {{.Name}}. Доступны два вида клавиатуры:\n\n• Стандартная - для обычного покера, использует Фибоначчи до 13;\n• Story Points клавиатура - клавиатура от 0.5 до 10 с шагом 0.5."
|
||||
bot_was_added: "Отлично, бот был добавлен в группу *{{.Name}}*."
|
||||
choose_keyboard: "⌨️ Выберите клавиатуру для голосований в группе *{{.Name}}*. Доступны два вида клавиатуры:\n\n• Стандартная - для обычного покера, использует Фибоначчи до 13;\n• Story Points клавиатура - клавиатура от 0.5 до 10 с шагом 0.5."
|
||||
standard_vote_keyboard: "⌨️ Стандартная poker-клавиатура"
|
||||
sp_vote_keyboard: "⌨️ Story Points poker-клавиатура"
|
||||
bot_was_removed_from_group: "⚠️ Бот был удален из группы {{.Name}}. Настройки группы были удалены. Для того, чтобы восстановить связь с группой добавьте бота в нее и произведите настройку повторно."
|
||||
bot_was_removed_from_group: "⚠️ Бот был удален из группы *{{.Name}}*. Настройки группы были удалены. Для того, чтобы восстановить связь с группой добавьте бота в нее и произведите настройку повторно."
|
||||
unknown_command: "❔ Неизвестная команда. Используйте /help для вывода списка доступных команд."
|
||||
help_output: "📄 Доступные команды:\n\n• /start - регистрация в боте;\n• /poll@vegapokerbot <task> - запускает poker в группе, работает только в подключенных группах;\n• /help - выводит эту справку."
|
||||
choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника."
|
||||
ask_for_redmine: "📕 Настроить интеграцию с Redmine?"
|
||||
redmine_will_not_be_configured: "⚠️ Интеграция с Redmine не будет настроена. Вы не сможете отправлять результаты poker в Redmine или получать информацию о задачах."
|
||||
redmine_will_not_be_configured: "👌 Интеграция с Redmine не будет настроена. Вы не сможете отправлять результаты poker в Redmine или получать информацию о задачах."
|
||||
redmine_hours_will_not_be_configured: "👌 Оценка временных затрат не будет передаваться в Redmine."
|
||||
redmine_hours_will_be_configured: "👌 Оценка временных затрат будет передаваться в Redmine."
|
||||
redmine_poker_will_not_be_configured: "👌 Результат голосования не будет передаваться в Redmine."
|
||||
redmine_poker_will_be_configured: "✔️ Готово, результат голосования будет передаваться в Redmine в поле *{{.Name}}*."
|
||||
chosen_keyboard: "Выбрана {{.Name}}."
|
||||
please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine."
|
||||
please_send_redmine_key: "Отправьте свой API-ключ, который будет использоваться ботом для взаимодействия с Redmine. Бот будет выполнять следующие действия:\n\n• получение данных задачи;\n• запись результата покера в задачу (если будет настроено в следующих шагах).\n\nКлюч доступа к API можно найти в левой части страницы по [этой ссылке](/my/account)."
|
||||
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, конвертация в часы автоматическая).\n\nБудете настраивать передачу этих данных?"
|
||||
redmine_was_connected: "✔️ Отлично, теперь Redmine подключен! Теперь при начале голосования и передаче в команду /poll@vegapokerbot номера задачи бот укажет в сообщении тему задачи и сгенерирует ссылку на задачу.\n\nБот может передавать результаты голосования напрямую в поля задачи в Redmine. Передаются следующие данные:\n\n• полученная в итоге голосования оценка;\n• конвертированная в часы оценка (если оценка в story points, конвертация в часы автоматическая)."
|
||||
should_send_poker_to_redmine: "🕑 Передавать в Redmine результат проведения poker без конвертации в часы? Потребуется указать название кастомного поля для передачи данных."
|
||||
specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки."
|
||||
should_also_send_hours: "Передавать в другое поле сконвертированный в часы результат оценки? Предполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 * 8 = 12."
|
||||
specify_second_result_field: "Укажите название поля, в которое будет передаваться результат оценки в часах."
|
||||
should_send_estimated_hours_redmine: "🕑 Передавать в поле *Оценка временных затрат* сконвертированный в часы результат оценки?\n\nПредполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 ✖️ 8 = 12."
|
||||
setup_done: "✔️ Готово, чат подключен к боту!\nИспользуйте команду /poll@vegapokerbot в подключенном чате для запуска голосования."
|
||||
yes: "✔️ Да"
|
||||
no: "✖️ Нет"
|
||||
|
Loading…
Reference in New Issue
Block a user