wip: chat configuration, removed useless code, now we're processing something
This commit is contained in:
parent
1e6407adfe
commit
d7b175fcd1
@ -4,7 +4,7 @@ import (
|
|||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
@ -14,6 +14,7 @@ import (
|
|||||||
type App struct {
|
type App struct {
|
||||||
Logger *zap.SugaredLogger
|
Logger *zap.SugaredLogger
|
||||||
Telegram *telego.Bot
|
Telegram *telego.Bot
|
||||||
|
BotProfile *telego.User
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Repositories *db.Repositories
|
Repositories *db.Repositories
|
||||||
Handlers map[handler.Type]handler.Handler
|
Handlers map[handler.Type]handler.Handler
|
||||||
@ -51,11 +52,15 @@ func (a *App) TG() *telego.Bot {
|
|||||||
return a.Telegram
|
return a.Telegram
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) TGProfile() *telego.User {
|
||||||
|
return a.BotProfile
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) Conf() *config.Config {
|
func (a *App) Conf() *config.Config {
|
||||||
return a.Config
|
return a.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) DB() fsmcontract.Repositories {
|
func (a *App) DB() iface.Repositories {
|
||||||
return &DBWrapper{a.Repositories}
|
return &DBWrapper{a.Repositories}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +99,10 @@ func (a *App) initDB() error {
|
|||||||
func (a *App) initTelegram() error {
|
func (a *App) initTelegram() error {
|
||||||
var err error
|
var err error
|
||||||
a.Telegram, err = telego.NewBot(a.Config.TelegramToken, logger.WrapForTelego(a.Logger, a.Config.Debug))
|
a.Telegram, err = telego.NewBot(a.Config.TelegramToken, logger.WrapForTelego(a.Logger, a.Config.Debug))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.BotProfile, err = a.Telegram.GetMe()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +111,7 @@ func (a *App) initHandlers() {
|
|||||||
handler.Noop: handler.NewNoopHandler(a.Logger, a.Config.Debug),
|
handler.Noop: handler.NewNoopHandler(a.Logger, a.Config.Debug),
|
||||||
handler.Message: handler.NewMessageHandler(a),
|
handler.Message: handler.NewMessageHandler(a),
|
||||||
handler.ChatMemberUpdated: handler.NewChatMemberUpdatedHandler(a),
|
handler.ChatMemberUpdated: handler.NewChatMemberUpdatedHandler(a),
|
||||||
|
handler.CallbackQuery: handler.NewCallbackQueryHandler(a),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,9 +119,12 @@ func (a *App) handler(update telego.Update) handler.Handler {
|
|||||||
if update.Message != nil {
|
if update.Message != nil {
|
||||||
return a.Handlers[handler.Message]
|
return a.Handlers[handler.Message]
|
||||||
}
|
}
|
||||||
if update.ChatMember != nil {
|
if update.MyChatMember != nil {
|
||||||
return a.Handlers[handler.ChatMemberUpdated]
|
return a.Handlers[handler.ChatMemberUpdated]
|
||||||
}
|
}
|
||||||
|
if update.CallbackQuery != nil {
|
||||||
|
return a.Handlers[handler.CallbackQuery]
|
||||||
|
}
|
||||||
return a.Handlers[handler.Noop]
|
return a.Handlers[handler.Noop]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,17 +2,21 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DBWrapper struct {
|
type DBWrapper struct {
|
||||||
r *db.Repositories
|
r *db.Repositories
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *DBWrapper) ForUser() fsmcontract.UserRepository {
|
func (w *DBWrapper) ForUser() iface.UserRepository {
|
||||||
return w.r.User
|
return w.r.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *DBWrapper) ForChat() fsmcontract.ChatRepository {
|
func (w *DBWrapper) ForChat() iface.ChatRepository {
|
||||||
return w.r.Chat
|
return w.r.Chat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *DBWrapper) ForIntegration() iface.IntegrationRepository {
|
||||||
|
return w.r.Integration
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
type Repositories struct {
|
type Repositories struct {
|
||||||
Chat *repository.Chat
|
Chat *repository.Chat
|
||||||
User *repository.User
|
User *repository.User
|
||||||
|
Integration *repository.Integration
|
||||||
}
|
}
|
||||||
|
|
||||||
func Connect(dsn string) (*gorm.DB, error) {
|
func Connect(dsn string) (*gorm.DB, error) {
|
||||||
@ -23,6 +24,7 @@ func InitRepositories(db *gorm.DB) *Repositories {
|
|||||||
return &Repositories{
|
return &Repositories{
|
||||||
Chat: repository.NewChat(db),
|
Chat: repository.NewChat(db),
|
||||||
User: repository.NewUser(db),
|
User: repository.NewUser(db),
|
||||||
|
Integration: repository.NewIntegration(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
CREATE TABLE "user" (
|
CREATE TABLE "user" (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
telegram_id BIGINT NOT NULL
|
telegram_id BIGINT NOT NULL,
|
||||||
|
chat_id BIGINT NOT NULL,
|
||||||
|
language VARCHAR(8)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE chat (
|
CREATE TABLE chat (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
telegram_id BIGINT NOT NULL,
|
telegram_id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
members JSON,
|
keyboard_type VARCHAR(6),
|
||||||
FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2,15 +2,14 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Chat struct {
|
type Chat struct {
|
||||||
ID uint64 `gorm:"primaryKey; autoIncrement" json:"id"`
|
ID uint64 `gorm:"primaryKey; autoIncrement" json:"id"`
|
||||||
TelegramID int64 `gorm:"column:telegram_id; not null" json:"telegramId"`
|
TelegramID int64 `gorm:"column:telegram_id; not null" json:"telegramId"`
|
||||||
UserID int64 `gorm:"column:user_id; not null"`
|
KeyboardType KeyboardType `gorm:"column:keyboard_type;"`
|
||||||
Members ChatMembers `gorm:"column:members; not null" json:"members"`
|
UserID uint64 `gorm:"column:user_id; not null"`
|
||||||
Integrations []Integration `gorm:"foreignKey:ChatID" json:"integrations"`
|
Integrations []Integration `gorm:"foreignKey:ChatID" json:"integrations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,35 +17,52 @@ func (Chat) TableName() string {
|
|||||||
return "chat"
|
return "chat"
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatMembers []int64
|
type KeyboardType uint8
|
||||||
|
|
||||||
func (cm *ChatMembers) Scan(value interface{}) error {
|
const (
|
||||||
|
StandardKeyboard KeyboardType = iota
|
||||||
|
StoryPointsKeyboard
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lbTypesToIds = map[string]KeyboardType{
|
||||||
|
"normal": StandardKeyboard,
|
||||||
|
"sp": StoryPointsKeyboard,
|
||||||
|
}
|
||||||
|
idsToKBTypes = map[KeyboardType]string{
|
||||||
|
StandardKeyboard: "normal",
|
||||||
|
StoryPointsKeyboard: "sp",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (it *KeyboardType) Scan(value interface{}) error {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case nil:
|
case nil:
|
||||||
*cm = nil
|
*it = 0 // or whatever default you want to set if the field is null in db
|
||||||
return nil
|
return nil
|
||||||
|
case KeyboardType:
|
||||||
|
*it = v
|
||||||
case string:
|
case string:
|
||||||
if err := json.Unmarshal([]byte(v), cm); err != nil {
|
// lookup the value from the map and assign it
|
||||||
return err
|
val, ok := lbTypesToIds[v]
|
||||||
}
|
if !ok {
|
||||||
case []byte:
|
return errors.New("invalid KeyboardType")
|
||||||
if err := json.Unmarshal(v, cm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
*it = val
|
||||||
default:
|
default:
|
||||||
return errors.New("invalid type")
|
return errors.New("invalid type")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm ChatMembers) Value() (driver.Value, error) {
|
func (it KeyboardType) Value() (driver.Value, error) {
|
||||||
if cm == nil {
|
val, ok := idsToKBTypes[it]
|
||||||
return "[]", nil
|
if ok {
|
||||||
|
return val, nil
|
||||||
}
|
}
|
||||||
jsonData, err := json.Marshal(cm)
|
return InvalidIntegration, nil
|
||||||
return string(jsonData), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ChatMembers) GormDataType() string {
|
func (KeyboardType) GormDataType() string {
|
||||||
return "json"
|
return "varchar(6)"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package model
|
|||||||
type User struct {
|
type User struct {
|
||||||
ID uint64 `gorm:"primary_key;auto_increment" json:"id"`
|
ID uint64 `gorm:"primary_key;auto_increment" json:"id"`
|
||||||
TelegramID int64 `gorm:"not null" json:"telegram_id"`
|
TelegramID int64 `gorm:"not null" json:"telegram_id"`
|
||||||
|
ChatID int64 `gorm:"not null" json:"chat_id"`
|
||||||
|
Language string `json:"language"`
|
||||||
Chats []Chat `gorm:"foreignKey:UserID" json:"chats"`
|
Chats []Chat `gorm:"foreignKey:UserID" json:"chats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,22 @@ func (c *Chat) ByID(id uint64) (*model.Chat, error) {
|
|||||||
return &chat, nil
|
return &chat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Chat) ByTelegramID(id int64) (*model.Chat, error) {
|
||||||
|
var chat model.Chat
|
||||||
|
if err := c.db.Model(model.Chat{TelegramID: id}).First(&chat).Error; err != nil {
|
||||||
|
return nil, util.HandleRecordNotFound(err)
|
||||||
|
}
|
||||||
|
return &chat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chat) ByTelegramIDWithIntegrations(id int64) (*model.Chat, error) {
|
||||||
|
var chat model.Chat
|
||||||
|
if err := c.db.Model(model.Chat{TelegramID: id}).Preload("Integrations").First(&chat).Error; err != nil {
|
||||||
|
return nil, util.HandleRecordNotFound(err)
|
||||||
|
}
|
||||||
|
return &chat, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Chat) ByIDWithIntegrations(id uint64) (*model.Chat, error) {
|
func (c *Chat) ByIDWithIntegrations(id uint64) (*model.Chat, error) {
|
||||||
var chat model.Chat
|
var chat model.Chat
|
||||||
if err := c.db.Model(&chat).Preload("Integrations").First(&chat, id).Error; err != nil {
|
if err := c.db.Model(&chat).Preload("Integrations").First(&chat, id).Error; err != nil {
|
||||||
@ -29,3 +45,11 @@ func (c *Chat) ByIDWithIntegrations(id uint64) (*model.Chat, error) {
|
|||||||
}
|
}
|
||||||
return &chat, nil
|
return &chat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Chat) Save(chat *model.Chat) error {
|
||||||
|
return c.db.Model(chat).Save(chat).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chat) Delete(chat *model.Chat) error {
|
||||||
|
return c.db.Model(&model.Chat{ID: chat.ID}).Delete(chat).Error
|
||||||
|
}
|
||||||
|
22
internal/db/repository/integration.go
Normal file
22
internal/db/repository/integration.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Integration struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIntegration(db *gorm.DB) *Integration {
|
||||||
|
return &Integration{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Integration) Save(integration *model.Integration) error {
|
||||||
|
return r.db.Model(integration).Save(integration).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Integration) DeleteForChat(id uint64) error {
|
||||||
|
return r.db.Where("chat_id = ?", id).Delete(&model.Integration{}).Error
|
||||||
|
}
|
@ -30,6 +30,14 @@ func (u *User) ByTelegramID(id int64) (*model.User, error) {
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) ByTelegramIDs(ids []int64) ([]model.User, error) {
|
||||||
|
var users []model.User
|
||||||
|
if err := u.db.Model(model.User{}).Where("telegram_id in (?)", ids).Find(&users).Error; err != nil {
|
||||||
|
return nil, util.HandleRecordNotFound(err)
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) ByIDWithChats(id uint64) (*model.User, error) {
|
func (u *User) ByIDWithChats(id uint64) (*model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := u.db.Model(&user).Preload("Chats").First(&user, id).Error; err != nil {
|
if err := u.db.Model(&user).Preload("Chats").First(&user, id).Error; err != nil {
|
||||||
@ -39,5 +47,5 @@ func (u *User) ByIDWithChats(id uint64) (*model.User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Save(user *model.User) error {
|
func (u *User) Save(user *model.User) error {
|
||||||
return u.db.Model(&model.User{}).Save(user).Error
|
return u.db.Model(user).Save(user).Error
|
||||||
}
|
}
|
||||||
|
151
internal/handler/callback_query_handler.go
Normal file
151
internal/handler/callback_query_handler.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||||
|
"github.com/mymmrac/telego"
|
||||||
|
tu "github.com/mymmrac/telego/telegoutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CallbackQueryHandler struct {
|
||||||
|
app iface.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCallbackQueryHandler(app iface.App) Handler {
|
||||||
|
return &CallbackQueryHandler{app: app}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackQueryHandler) Handle(wh telego.Update) error {
|
||||||
|
cq := wh.CallbackQuery
|
||||||
|
user, err := h.app.DB().ForUser().ByTelegramID(cq.From.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if user != nil && user.ID > 0 {
|
||||||
|
return h.handleSetupKey(cq, user)
|
||||||
|
}
|
||||||
|
return errors.New("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackQueryHandler) handleSetupKey(cq *telego.CallbackQuery, user *model.User) error {
|
||||||
|
var pl util.Payload
|
||||||
|
if err := json.Unmarshal([]byte(cq.Data), &pl); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch pl.Action {
|
||||||
|
case util.PayloadActionChooseKeyboard:
|
||||||
|
return h.handleChooseKeyboard(pl, cq.Message.GetMessageID(), user)
|
||||||
|
case util.PayloadActionYesNo:
|
||||||
|
answer := pl.YesNoAnswer()
|
||||||
|
switch answer.Type {
|
||||||
|
case util.QuestionTypeRedmine:
|
||||||
|
return h.handleAnswerRedmine(answer, cq.Message.GetMessageID(), user)
|
||||||
|
case util.QuestionTypeRedmineHours:
|
||||||
|
return h.handleAnswerRedmineHours(answer, pl.Chat, cq.Message.GetMessageID(), user)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int, user *model.User) error {
|
||||||
|
cr := h.app.DB().ForChat()
|
||||||
|
result := pl.KeyboardChoice()
|
||||||
|
if pl.User != user.TelegramID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
chat, err := cr.ByTelegramID(pl.Chat)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if chat == nil || chat.ID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
chat.KeyboardType = result.Type
|
||||||
|
if err := cr.Save(chat); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kbTypeName := "standard_vote_keyboard"
|
||||||
|
if result.Type == model.StoryPointsKeyboard {
|
||||||
|
kbTypeName = "sp_vote_keyboard"
|
||||||
|
}
|
||||||
|
loc := h.app.Localizer(user.Language)
|
||||||
|
_, _ = h.app.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||||
|
ChatID: tu.ID(user.ChatID),
|
||||||
|
MessageID: msgID,
|
||||||
|
Text: loc.Template("chosen_keyboard", map[string]interface{}{"Name": loc.Message(kbTypeName)}),
|
||||||
|
})
|
||||||
|
_, err = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||||
|
ChatID: tu.ID(user.ChatID),
|
||||||
|
Text: loc.Message("ask_for_redmine"),
|
||||||
|
ParseMode: telego.ModeMarkdown,
|
||||||
|
ReplyMarkup: &telego.InlineKeyboardMarkup{InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: loc.Message("yes"),
|
||||||
|
CallbackData: util.NewRedmineQuestionPayload(user.TelegramID, chat.TelegramID, true).String(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: loc.Message("no"),
|
||||||
|
CallbackData: util.NewRedmineQuestionPayload(user.TelegramID, chat.TelegramID, false).String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackQueryHandler) handleAnswerRedmine(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_will_not_be_configured"),
|
||||||
|
})
|
||||||
|
return sendSetupDoneMessage(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CallbackQueryHandler) handleAnswerRedmineHours(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{
|
||||||
|
ChatID: tu.ID(user.ChatID),
|
||||||
|
MessageID: msgID,
|
||||||
|
Text: loc.Message("redmine_hours_will_not_be_configured"),
|
||||||
|
})
|
||||||
|
return sendSetupDoneMessage(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"),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
@ -1,19 +1,202 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
|
tu "github.com/mymmrac/telego/telegoutil"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MaxChatMembers = 32
|
||||||
|
|
||||||
type ChatMemberUpdatedHandler struct {
|
type ChatMemberUpdatedHandler struct {
|
||||||
app fsmcontract.App
|
app iface.App
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChatMemberUpdatedHandler(app fsmcontract.App) *ChatMemberUpdatedHandler {
|
func NewChatMemberUpdatedHandler(app iface.App) *ChatMemberUpdatedHandler {
|
||||||
return &ChatMemberUpdatedHandler{app: app}
|
return &ChatMemberUpdatedHandler{app: app}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChatMemberUpdatedHandler) Handle(telego.Update) error {
|
func (h *ChatMemberUpdatedHandler) Handle(wh telego.Update) error {
|
||||||
// TODO implement me
|
cm := wh.MyChatMember
|
||||||
panic("implement me")
|
if !cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.app.TGProfile().ID &&
|
||||||
|
cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.app.TGProfile().ID {
|
||||||
|
return h.handleAddToChat(wh.MyChatMember.Chat)
|
||||||
|
}
|
||||||
|
if cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.app.TGProfile().ID &&
|
||||||
|
!cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.app.TGProfile().ID {
|
||||||
|
return h.handleRemoveFromChat(wh.MyChatMember.Chat)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.leaveChat(wh.MyChatMember.Chat.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.getRegisteredAdmin(tgChat.ID)
|
||||||
|
if err != nil {
|
||||||
|
h.sendInternalError(tgChat.ID, nil)
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if user == nil || user.ID == 0 {
|
||||||
|
_, err = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||||
|
ChatID: tu.ID(tgChat.ID),
|
||||||
|
Text: h.app.Localizer(language.English.String()).Message("you_should_register_first"),
|
||||||
|
ParseMode: telego.ModeMarkdown,
|
||||||
|
})
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loc := h.app.Localizer(user.Language)
|
||||||
|
|
||||||
|
totalMembers, err := h.app.TG().GetChatMemberCount(&telego.GetChatMemberCountParams{
|
||||||
|
ChatID: tu.ID(tgChat.ID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.sendInternalError(tgChat.ID, nil)
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *totalMembers > MaxChatMembers {
|
||||||
|
_, err = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||||
|
ChatID: tu.ID(tgChat.ID),
|
||||||
|
Text: loc.Template("too_many_members_in_the_group", map[string]interface{}{"Limit": MaxChatMembers}),
|
||||||
|
ParseMode: telego.ModeMarkdown,
|
||||||
|
})
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if chat == nil || chat.ID == 0 {
|
||||||
|
chat = &model.Chat{
|
||||||
|
TelegramID: tgChat.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
}
|
||||||
|
err := cr.Save(chat)
|
||||||
|
if err != nil {
|
||||||
|
h.sendInternalError(tgChat.ID, loc)
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chat.UserID = user.ID
|
||||||
|
err := h.app.DB().ForIntegration().DeleteForChat(chat.ID)
|
||||||
|
if err != nil {
|
||||||
|
h.sendInternalError(tgChat.ID, loc)
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = cr.Save(chat)
|
||||||
|
if err != nil {
|
||||||
|
h.sendInternalError(tgChat.ID, loc)
|
||||||
|
h.leaveChat(tgChat.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||||
|
ChatID: tu.ID(user.ChatID),
|
||||||
|
Text: loc.Template("bot_was_added", map[string]interface{}{"Name": tgChat.Title}),
|
||||||
|
ParseMode: telego.ModeMarkdown,
|
||||||
|
})
|
||||||
|
|
||||||
|
return h.sendKeyboardChooser(tgChat, user, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatMemberUpdatedHandler) handleRemoveFromChat(tgChat telego.Chat) error {
|
||||||
|
cr := h.app.DB().ForChat()
|
||||||
|
chat, err := cr.ByTelegramID(tgChat.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if chat != nil && chat.ID > 0 {
|
||||||
|
user, _ := h.app.DB().ForUser().ByID(chat.UserID)
|
||||||
|
if user != nil && user.ID > 0 && user.ChatID > 0 {
|
||||||
|
_, _ = h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||||
|
ChatID: tu.ID(user.ChatID),
|
||||||
|
Text: h.app.Localizer(user.Language).Template(
|
||||||
|
"bot_was_removed_from_group", map[string]interface{}{"Name": tgChat.Title}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cr.Delete(chat)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatMemberUpdatedHandler) sendKeyboardChooser(chat telego.Chat, user *model.User, loc locale.Localizer) error {
|
||||||
|
_, err := h.app.TG().SendMessage(&telego.SendMessageParams{
|
||||||
|
ChatID: tu.ID(user.ChatID),
|
||||||
|
Text: loc.Template("choose_keyboard", map[string]interface{}{"Name": chat.Title}),
|
||||||
|
ParseMode: telego.ModeMarkdown,
|
||||||
|
ReplyMarkup: &telego.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: loc.Message("standard_vote_keyboard"),
|
||||||
|
CallbackData: util.NewKeyboardChooserPayload(
|
||||||
|
user.TelegramID, chat.ID, model.StandardKeyboard).String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: loc.Message("sp_vote_keyboard"),
|
||||||
|
CallbackData: util.NewKeyboardChooserPayload(
|
||||||
|
user.TelegramID, chat.ID, model.StoryPointsKeyboard).String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatMemberUpdatedHandler) getRegisteredAdmin(chatID int64) (*model.User, error) {
|
||||||
|
admins, err := h.app.TG().GetChatAdministrators(&telego.GetChatAdministratorsParams{
|
||||||
|
ChatID: tu.ID(chatID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
adminIDs := make([]int64, len(admins))
|
||||||
|
for i, admin := range admins {
|
||||||
|
adminIDs[i] = admin.MemberUser().ID
|
||||||
|
}
|
||||||
|
dbAdmins, err := h.app.DB().ForUser().ByTelegramIDs(adminIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(dbAdmins) > 0 {
|
||||||
|
return &dbAdmins[0], nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatMemberUpdatedHandler) leaveChat(chatID int64) {
|
||||||
|
_ = h.app.TG().LeaveChat(&telego.LeaveChatParams{
|
||||||
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
package fsmcontract
|
|
||||||
|
|
||||||
import "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
|
||||||
|
|
||||||
type Repositories interface {
|
|
||||||
ForUser() UserRepository
|
|
||||||
ForChat() ChatRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserRepository interface {
|
|
||||||
ByTelegramID(id int64) (*model.User, error)
|
|
||||||
Save(user *model.User) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChatRepository interface{}
|
|
@ -1,7 +0,0 @@
|
|||||||
package fsmcontract
|
|
||||||
|
|
||||||
type Machine interface {
|
|
||||||
Reset()
|
|
||||||
Handle()
|
|
||||||
HandleError(error)
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package fsmcontract
|
|
||||||
|
|
||||||
type State interface {
|
|
||||||
Handle(ctx Context) (State, Context, error)
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
package fsmcore
|
|
||||||
|
|
||||||
import "sync/atomic"
|
|
||||||
|
|
||||||
type AtomicValue[T any] struct {
|
|
||||||
ptr atomic.Pointer[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAtomicValue[T any](val T) *AtomicValue[T] {
|
|
||||||
value := &AtomicValue[T]{}
|
|
||||||
value.ptr.Store(&val)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *AtomicValue[T]) Load() (result T) {
|
|
||||||
val := v.ptr.Load()
|
|
||||||
if val == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return *val
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *AtomicValue[T]) Store(value T) {
|
|
||||||
v.ptr.Store(&value)
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package fsmcore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
MachineErrorHandler func(fsmcontract.Machine, fsmcontract.Context, error)
|
|
||||||
NewContextFunc func() fsmcontract.Context
|
|
||||||
)
|
|
||||||
|
|
||||||
type Machine struct {
|
|
||||||
ctx *AtomicValue[fsmcontract.Context]
|
|
||||||
entryState fsmcontract.State
|
|
||||||
state *AtomicValue[fsmcontract.State]
|
|
||||||
newContextFunc NewContextFunc
|
|
||||||
errHandler MachineErrorHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMachine(ctx fsmcontract.Context, entryState fsmcontract.State,
|
|
||||||
newContextFunc NewContextFunc, errHandler MachineErrorHandler) *Machine {
|
|
||||||
return &Machine{
|
|
||||||
ctx: NewAtomicValue[fsmcontract.Context](ctx),
|
|
||||||
entryState: entryState,
|
|
||||||
state: NewAtomicValue[fsmcontract.State](entryState),
|
|
||||||
newContextFunc: newContextFunc,
|
|
||||||
errHandler: errHandler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Machine) Handle() {
|
|
||||||
ctx := m.ctx.Load()
|
|
||||||
state := m.state.Load()
|
|
||||||
next, newCtx, err := state.Handle(ctx)
|
|
||||||
if err != nil {
|
|
||||||
m.HandleError(err)
|
|
||||||
}
|
|
||||||
if next != state {
|
|
||||||
m.state.Store(next)
|
|
||||||
}
|
|
||||||
m.ctx.Store(newCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Machine) Reset() {
|
|
||||||
var ctx fsmcontract.Context
|
|
||||||
if m.newContextFunc != nil {
|
|
||||||
ctx = m.newContextFunc()
|
|
||||||
}
|
|
||||||
m.ctx.Store(ctx)
|
|
||||||
m.state.Store(m.entryState)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Machine) HandleError(err error) {
|
|
||||||
if m.errHandler != nil {
|
|
||||||
m.errHandler(m, m.ctx.Load(), err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package fsmcore
|
|
||||||
|
|
||||||
import "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
|
||||||
|
|
||||||
type HandleFunc func(ctx fsmcontract.Context) (fsmcontract.State, fsmcontract.Context, error)
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
next HandleFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) Handle(ctx fsmcontract.Context) (fsmcontract.State, fsmcontract.Context, error) {
|
|
||||||
return s.next(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewState(next HandleFunc) fsmcontract.State {
|
|
||||||
return &State{next: next}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
|
||||||
"github.com/mymmrac/telego"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
Application fsmcontract.App
|
|
||||||
Update telego.Update
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) App() fsmcontract.App {
|
|
||||||
return c.Application
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Data() map[string]interface{} {
|
|
||||||
return map[string]interface{}{"update": c.Update}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package machine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
userMachines = newStore[int64, fsmcontract.Machine]()
|
|
||||||
pollMachines = newStore[int64, fsmcontract.Machine]()
|
|
||||||
)
|
|
||||||
|
|
||||||
type store[K comparable, V any] struct {
|
|
||||||
m map[K]V
|
|
||||||
l sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStore[K comparable, V any]() *store[K, V] {
|
|
||||||
return &store[K, V]{m: map[K]V{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store[K, V]) Store(k K, v V) {
|
|
||||||
defer s.l.Unlock()
|
|
||||||
s.l.Lock()
|
|
||||||
s.m[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store[K, V]) Load(k K) (v V, ok bool) {
|
|
||||||
defer s.l.RUnlock()
|
|
||||||
s.l.RLock()
|
|
||||||
v, ok = s.m[k]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ForUser(id int64) fsmcontract.Machine {
|
|
||||||
val, ok := userMachines.Load(id)
|
|
||||||
if !ok {
|
|
||||||
machine := newUserMachine()
|
|
||||||
userMachines.Store(id, machine)
|
|
||||||
return machine
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
func ForPoll(id int64) fsmcontract.Machine {
|
|
||||||
val, ok := pollMachines.Load(id)
|
|
||||||
if !ok {
|
|
||||||
machine := newPollMachine()
|
|
||||||
pollMachines.Store(id, machine)
|
|
||||||
return machine
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUserMachine() fsmcontract.Machine {
|
|
||||||
// todo implement this
|
|
||||||
panic("not implemented yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPollMachine() fsmcontract.Machine {
|
|
||||||
// todo implement this
|
|
||||||
panic("not implemented yet")
|
|
||||||
}
|
|
@ -10,6 +10,7 @@ const (
|
|||||||
Noop Type = iota
|
Noop Type = iota
|
||||||
Message
|
Message
|
||||||
ChatMemberUpdated
|
ChatMemberUpdated
|
||||||
|
CallbackQuery
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package fsmcontract
|
package iface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
||||||
@ -7,14 +7,10 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context interface {
|
|
||||||
App() App
|
|
||||||
Data() map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type App interface {
|
type App interface {
|
||||||
Log() *zap.SugaredLogger
|
Log() *zap.SugaredLogger
|
||||||
TG() *telego.Bot
|
TG() *telego.Bot
|
||||||
|
TGProfile() *telego.User
|
||||||
Conf() *config.Config
|
Conf() *config.Config
|
||||||
DB() Repositories
|
DB() Repositories
|
||||||
Localizer(string) locale.Localizer
|
Localizer(string) locale.Localizer
|
@ -1,17 +1,16 @@
|
|||||||
package iface
|
package iface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
App fsmcontract.App
|
App App
|
||||||
UserID int64
|
UserID int64
|
||||||
ChatID int64
|
ChatID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBase(app fsmcontract.App, userID int64, chatID int64) Base {
|
func NewBase(app App, userID int64, chatID int64) Base {
|
||||||
return Base{app, userID, chatID}
|
return Base{app, userID, chatID}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
internal/handler/iface/context.go
Normal file
6
internal/handler/iface/context.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package iface
|
||||||
|
|
||||||
|
type Context interface {
|
||||||
|
App() App
|
||||||
|
Data() map[string]interface{}
|
||||||
|
}
|
29
internal/handler/iface/repositories.go
Normal file
29
internal/handler/iface/repositories.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package iface
|
||||||
|
|
||||||
|
import "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||||
|
|
||||||
|
type Repositories interface {
|
||||||
|
ForUser() UserRepository
|
||||||
|
ForChat() ChatRepository
|
||||||
|
ForIntegration() IntegrationRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
ByID(id uint64) (*model.User, error)
|
||||||
|
ByTelegramID(id int64) (*model.User, error)
|
||||||
|
ByTelegramIDs(ids []int64) ([]model.User, error)
|
||||||
|
Save(user *model.User) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatRepository interface {
|
||||||
|
ByID(id uint64) (*model.Chat, error)
|
||||||
|
ByTelegramID(id int64) (*model.Chat, error)
|
||||||
|
ByTelegramIDWithIntegrations(id int64) (*model.Chat, error)
|
||||||
|
Save(chat *model.Chat) error
|
||||||
|
Delete(chat *model.Chat) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntegrationRepository interface {
|
||||||
|
Save(integration *model.Integration) error
|
||||||
|
DeleteForChat(id uint64) error
|
||||||
|
}
|
@ -2,34 +2,37 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/wizard"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/wizard"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MessageHandler struct {
|
type MessageHandler struct {
|
||||||
app fsmcontract.App
|
app iface.App
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessageHandler(app fsmcontract.App) Handler {
|
func NewMessageHandler(app iface.App) Handler {
|
||||||
return &MessageHandler{app: app}
|
return &MessageHandler{app: app}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *MessageHandler) Handle(wh telego.Update) error {
|
func (h *MessageHandler) Handle(wh telego.Update) error {
|
||||||
if wh.Message != nil {
|
|
||||||
if wh.Message.From != nil &&
|
if wh.Message.From != nil &&
|
||||||
wh.Message.Chat.Type == telego.ChatTypePrivate &&
|
wh.Message.Chat.Type == telego.ChatTypePrivate {
|
||||||
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)
|
||||||
|
}
|
||||||
|
return wizard.NewUnknownCommand(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove debug statement below.
|
// TODO: Remove debug statement below.
|
||||||
h.app.Log().Debugf("New Message: %s", func(msg *telego.Message) string {
|
h.app.Log().Debugf("New Message: %s", func(msg *telego.Message) string {
|
||||||
data, _ := json.Marshal(msg)
|
data, _ := json.Marshal(msg)
|
||||||
return string(data)
|
return string(data)
|
||||||
}(wh.Message))
|
}(wh.Message))
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package machine
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/machine/dto"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/maypok86/otter"
|
"github.com/maypok86/otter"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MemberVote struct {
|
type MemberVote struct {
|
||||||
dto.Member
|
util.Member
|
||||||
Vote float32
|
Vote float32
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ type PollResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PollState struct {
|
type PollState struct {
|
||||||
Members []dto.Member
|
Members []util.Member
|
||||||
Votes []MemberVote
|
Votes []MemberVote
|
||||||
Result PollResult
|
Result PollResult
|
||||||
}
|
}
|
@ -1,25 +1,22 @@
|
|||||||
package dto
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PayloadAction uint8
|
type PayloadAction uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PayloadActionNext = iota
|
PayloadActionYesNo = iota
|
||||||
PayloadActionPrevious
|
|
||||||
PayloadActionAddMember
|
|
||||||
PayloadActionRemoveMember
|
|
||||||
PayloadActionYesNo
|
|
||||||
PayloadActionVote
|
PayloadActionVote
|
||||||
|
PayloadActionChooseKeyboard
|
||||||
)
|
)
|
||||||
|
|
||||||
type QuestionType uint8
|
type QuestionType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
QuestionTypeChatMembers QuestionType = iota
|
QuestionTypeRedmine = iota
|
||||||
QuestionTypeRedmine
|
|
||||||
QuestionTypeRedmineHours
|
QuestionTypeRedmineHours
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,11 +27,44 @@ type Payload struct {
|
|||||||
Data json.RawMessage `json:"d,omitempty"`
|
Data json.RawMessage `json:"d,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Payload) String() string {
|
||||||
|
data, _ := json.Marshal(&p)
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Payload) Vote() (val Vote) {
|
||||||
|
if p.Action != PayloadActionVote {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(p.Data, &val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Payload) KeyboardChoice() (val KBChooserData) {
|
||||||
|
if p.Action != PayloadActionVote {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(p.Data, &val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Payload) YesNoAnswer() (val Answer) {
|
||||||
|
if p.Action != PayloadActionYesNo {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(p.Data, &val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type Member struct {
|
type Member struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KBChooserData struct {
|
||||||
|
Type model.KeyboardType `json:"k"`
|
||||||
|
}
|
||||||
|
|
||||||
type Vote struct {
|
type Vote struct {
|
||||||
Vote float32 `json:"v"`
|
Vote float32 `json:"v"`
|
||||||
}
|
}
|
||||||
@ -44,37 +74,14 @@ type Answer struct {
|
|||||||
Result bool `json:"r"`
|
Result bool `json:"r"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNextPageMembersPayload(userID, chatID int64) *Payload {
|
func NewKeyboardChooserPayload(userID, chatID int64, kbType model.KeyboardType) *Payload {
|
||||||
return &Payload{
|
return &Payload{
|
||||||
Action: PayloadActionNext,
|
Action: PayloadActionChooseKeyboard,
|
||||||
User: userID,
|
User: userID,
|
||||||
Chat: chatID,
|
Chat: chatID,
|
||||||
}
|
Data: marshal(KBChooserData{
|
||||||
}
|
Type: kbType,
|
||||||
|
}),
|
||||||
func NewPrevPageMembersPayload(userID, chatID int64) *Payload {
|
|
||||||
return &Payload{
|
|
||||||
Action: PayloadActionPrevious,
|
|
||||||
User: userID,
|
|
||||||
Chat: chatID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAddMemberPayload(userID, chatID, memberID int64, memberName string) *Payload {
|
|
||||||
return &Payload{
|
|
||||||
Action: PayloadActionAddMember,
|
|
||||||
User: userID,
|
|
||||||
Chat: chatID,
|
|
||||||
Data: marshal(Member{ID: memberID, Name: memberName}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRemoveMemberPayload(userID, chatID, memberID int64, memberName string) *Payload {
|
|
||||||
return &Payload{
|
|
||||||
Action: PayloadActionRemoveMember,
|
|
||||||
User: userID,
|
|
||||||
Chat: chatID,
|
|
||||||
Data: marshal(Member{ID: memberID, Name: memberName}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,15 +93,6 @@ func NewVotePayload(chatID int64, vote float32) *Payload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfirmMembersPayload(userID int64, chatID int64) *Payload {
|
|
||||||
return &Payload{
|
|
||||||
Action: PayloadActionYesNo,
|
|
||||||
User: userID,
|
|
||||||
Chat: chatID,
|
|
||||||
Data: marshal(Answer{Type: QuestionTypeChatMembers, Result: true}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRedmineQuestionPayload(userID int64, chatID int64, isYes bool) *Payload {
|
func NewRedmineQuestionPayload(userID int64, chatID int64, isYes bool) *Payload {
|
||||||
return &Payload{
|
return &Payload{
|
||||||
Action: PayloadActionYesNo,
|
Action: PayloadActionYesNo,
|
24
internal/handler/wizard/help_command.go
Normal file
24
internal/handler/wizard/help_command.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package wizard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||||
|
"github.com/mymmrac/telego"
|
||||||
|
tu "github.com/mymmrac/telego/telegoutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelpCommand struct {
|
||||||
|
UnknownCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHelpCommand(app iface.App, userID, chatID int64) *HelpCommand {
|
||||||
|
return &HelpCommand{*NewUnknownCommand(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"),
|
||||||
|
ParseMode: telego.ModeMarkdown,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
@ -2,7 +2,6 @@ package wizard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
tu "github.com/mymmrac/telego/telegoutil"
|
tu "github.com/mymmrac/telego/telegoutil"
|
||||||
@ -12,7 +11,7 @@ type Register struct {
|
|||||||
iface.Base
|
iface.Base
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegister(app fsmcontract.App, userID, chatID int64) *Register {
|
func NewRegister(app iface.App, userID, chatID int64) *Register {
|
||||||
return &Register{iface.NewBase(app, userID, chatID)}
|
return &Register{iface.NewBase(app, userID, chatID)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +24,18 @@ func (h *Register) Handle(wh telego.Update) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user != nil && user.ID > 0 {
|
if user != nil && user.ID > 0 {
|
||||||
|
var shouldUpdate bool
|
||||||
|
if user.ChatID != wh.Message.Chat.ID {
|
||||||
|
user.ChatID = wh.Message.Chat.ID
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
|
if user.Language != wh.Message.From.LanguageCode {
|
||||||
|
user.Language = wh.Message.From.LanguageCode
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
|
if shouldUpdate {
|
||||||
|
_ = userRepo.Save(user)
|
||||||
|
}
|
||||||
_, 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: loc.Message("welcome"),
|
Text: loc.Message("welcome"),
|
||||||
@ -35,6 +46,8 @@ func (h *Register) Handle(wh telego.Update) error {
|
|||||||
|
|
||||||
err = userRepo.Save(&model.User{
|
err = userRepo.Save(&model.User{
|
||||||
TelegramID: wh.Message.From.ID,
|
TelegramID: wh.Message.From.ID,
|
||||||
|
ChatID: wh.Message.Chat.ID,
|
||||||
|
Language: wh.Message.From.LanguageCode,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
|
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||||
|
38
internal/handler/wizard/unknown_command.go
Normal file
38
internal/handler/wizard/unknown_command.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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 {
|
||||||
|
iface.Base
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnknownCommand(app iface.App, userID, chatID int64) *UnknownCommand {
|
||||||
|
return &UnknownCommand{iface.NewBase(app, userID, chatID)}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
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)
|
||||||
|
}
|
@ -1,20 +1,26 @@
|
|||||||
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."
|
||||||
|
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 chat \"{{.Name}}\". Choose chat members who will participate in the poker."
|
bot_was_added: "Great, the bot has been added to the group \"{{.Name}}\"."
|
||||||
previous: "◀️ Back"
|
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."
|
||||||
next: "Next ▶️"
|
standard_vote_keyboard: "⌨️ Standard poker keyboard"
|
||||||
choose_keyboard: "⌨️ Choose a keyboard for poker. 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."
|
sp_vote_keyboard: "⌨️ Story Points poker keyboard"
|
||||||
standard_vote_keyboard: "Standard 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."
|
||||||
sp_vote_keyboard: "Story Points poker keyboard"
|
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."
|
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_hours_will_not_be_configured: "Vote results won't be sent to Redmine as hours."
|
||||||
|
chosen_keyboard: "{{.Name}} has been selected."
|
||||||
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](/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](/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).\n\nDo you want to send this data to Redmine at the end of the poker?"
|
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."
|
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."
|
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."
|
specify_second_result_field: "Specify the name of the field where the poker result in hours will be writen."
|
||||||
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@vegapokerbot in the connected chat to start a vote."
|
||||||
yes: "✔️ Yes"
|
yes: "✔️ Yes"
|
||||||
no: "✖️ No"
|
no: "✖️ No"
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
|
welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
|
||||||
|
you_should_register_first: "Перед добавлением бота в группу необходимо зарегистрироваться в @vegapokerbot."
|
||||||
|
too_many_members_in_the_group: "Бот не поддерживает группы, в которых количество участников превышает {{.Limit}}."
|
||||||
internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже."
|
internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже."
|
||||||
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере."
|
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\"."
|
||||||
previous: "◀️ Назад"
|
choose_keyboard: "⌨️ Выберите клавиатуру для голосований в группе {{.Name}}. Доступны два вида клавиатуры:\n\n• Стандартная - для обычного покера, использует Фибоначчи до 13;\n• Story Points клавиатура - клавиатура от 0.5 до 10 с шагом 0.5."
|
||||||
next: "Вперед ▶️"
|
standard_vote_keyboard: "⌨️ Стандартная poker-клавиатура"
|
||||||
choose_keyboard: "⌨️ Выберите клавиатуру для голосований в чате. Доступны два вида клавиатуры:\n\n• Стандартная - для обычного покера, использует Фибоначчи до 13;\n• Story Points клавиатура - клавиатура от 0.5 до 10 с шагом 0.5."
|
sp_vote_keyboard: "⌨️ Story Points poker-клавиатура"
|
||||||
standard_vote_keyboard: "Стандартная poker-клавиатура"
|
bot_was_removed_from_group: "⚠️ Бот был удален из группы {{.Name}}. Настройки группы были удалены. Для того, чтобы восстановить связь с группой добавьте бота в нее и произведите настройку повторно."
|
||||||
sp_vote_keyboard: "Story Points poker-клавиатура"
|
unknown_command: "❔ Неизвестная команда. Используйте /help для вывода списка доступных команд."
|
||||||
|
help_output: "📄 Доступные команды:\n\n• /start - регистрация в боте;\n• /poll@vegapokerbot <task> - запускает poker в группе, работает только в подключенных группах;\n• /help - выводит эту справку."
|
||||||
choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника."
|
choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника."
|
||||||
ask_for_redmine: "📕 Настроить интеграцию с Redmine?"
|
ask_for_redmine: "📕 Настроить интеграцию с Redmine?"
|
||||||
|
redmine_will_not_be_configured: "⚠️ Интеграция с Redmine не будет настроена. Вы не сможете отправлять результаты poker в Redmine или получать информацию о задачах."
|
||||||
|
chosen_keyboard: "Выбрана {{.Name}}."
|
||||||
please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine."
|
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 можно найти в левой части страницы по [этой ссылке](/my/account)."
|
||||||
invalid_redmine_credentials: "⚠️ Не удалось проверить связь с Redmine. Будете пробовать настроить заново?"
|
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, конвертация в часы автоматическая).\n\nБудете настраивать передачу этих данных?"
|
||||||
specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки."
|
specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки."
|
||||||
should_also_send_hours: "Передавать в другое поле сконвертированный в часы результат оценки? Предполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 * 8 = 12."
|
should_also_send_hours: "Передавать в другое поле сконвертированный в часы результат оценки? Предполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 * 8 = 12."
|
||||||
specify_second_result_field: "Укажите название поля, в которое будет передаваться результат оценки в часах."
|
specify_second_result_field: "Укажите название поля, в которое будет передаваться результат оценки в часах."
|
||||||
setup_done: "✔️ Готово, чат подключен к боту!\nИспользуйте команду`/poll@vegapokerbot` в подключенном чате для запуска голосования."
|
setup_done: "✔️ Готово, чат подключен к боту!\nИспользуйте команду /poll@vegapokerbot в подключенном чате для запуска голосования."
|
||||||
yes: "✔️ Да"
|
yes: "✔️ Да"
|
||||||
no: "✖️ Нет"
|
no: "✖️ Нет"
|
||||||
|
Loading…
Reference in New Issue
Block a user