first webhook processor, fix errors
This commit is contained in:
parent
0e2cc70a09
commit
1e6407adfe
@ -1,10 +1,10 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
|
||||
"github.com/mymmrac/telego"
|
||||
@ -55,8 +55,8 @@ func (a *App) Conf() *config.Config {
|
||||
return a.Config
|
||||
}
|
||||
|
||||
func (a *App) DB() *db.Repositories {
|
||||
return a.Repositories
|
||||
func (a *App) DB() fsmcontract.Repositories {
|
||||
return &DBWrapper{a.Repositories}
|
||||
}
|
||||
|
||||
func (a *App) Localizer(lang string) locale.Localizer {
|
||||
@ -122,7 +122,16 @@ func (a *App) longPoll() error {
|
||||
}
|
||||
defer a.Telegram.StopLongPolling()
|
||||
for update := range updates {
|
||||
fmt.Printf("Update: %+v\n", update)
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
a.Logger.Errorf("recovered from panic inside the handler: %v", r)
|
||||
}
|
||||
}()
|
||||
if err := a.handler(update).Handle(update); err != nil {
|
||||
a.Logger.Errorf("error while handling the update: %s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
18
internal/app/db_wrapper.go
Normal file
18
internal/app/db_wrapper.go
Normal file
@ -0,0 +1,18 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||
)
|
||||
|
||||
type DBWrapper struct {
|
||||
r *db.Repositories
|
||||
}
|
||||
|
||||
func (w *DBWrapper) ForUser() fsmcontract.UserRepository {
|
||||
return w.r.User
|
||||
}
|
||||
|
||||
func (w *DBWrapper) ForChat() fsmcontract.ChatRepository {
|
||||
return w.r.Chat
|
||||
}
|
@ -8,8 +8,8 @@ import (
|
||||
|
||||
type Chat struct {
|
||||
ID uint64 `gorm:"primaryKey; autoIncrement" json:"id"`
|
||||
TelegramID uint64 `gorm:"column:telegram_id; not null" json:"telegramId"`
|
||||
UserID uint64 `gorm:"column:user_id; not null"`
|
||||
TelegramID int64 `gorm:"column:telegram_id; not null" json:"telegramId"`
|
||||
UserID int64 `gorm:"column:user_id; not null"`
|
||||
Members ChatMembers `gorm:"column:members; not null" json:"members"`
|
||||
Integrations []Integration `gorm:"foreignKey:ChatID" json:"integrations"`
|
||||
}
|
||||
@ -18,7 +18,7 @@ func (Chat) TableName() string {
|
||||
return "chat"
|
||||
}
|
||||
|
||||
type ChatMembers []uint64
|
||||
type ChatMembers []int64
|
||||
|
||||
func (cm *ChatMembers) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
@ -41,8 +41,12 @@ func (cm *ChatMembers) Scan(value interface{}) error {
|
||||
|
||||
func (cm ChatMembers) Value() (driver.Value, error) {
|
||||
if cm == nil {
|
||||
return nil, nil
|
||||
return "[]", nil
|
||||
}
|
||||
jsonData, err := json.Marshal(cm)
|
||||
return string(jsonData), err
|
||||
}
|
||||
|
||||
func (ChatMembers) GormDataType() string {
|
||||
return "json"
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
type Integration struct {
|
||||
ID uint64 `gorm:"primaryKey; autoIncrement;"`
|
||||
Type IntegrationType `gorm:"column:type; not null"`
|
||||
ChatID uint64 `gorm:"column:chat_id; not null"`
|
||||
ChatID int64 `gorm:"column:chat_id; not null"`
|
||||
Params datatypes.JSONMap `gorm:"column:params; not null"`
|
||||
}
|
||||
|
||||
@ -60,5 +60,9 @@ func (it IntegrationType) Value() (driver.Value, error) {
|
||||
if ok {
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.New("invalid IntegrationType")
|
||||
return InvalidIntegration, nil
|
||||
}
|
||||
|
||||
func (IntegrationType) GormDataType() string {
|
||||
return "varchar(24)"
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package model
|
||||
|
||||
type User struct {
|
||||
ID uint64 `gorm:"primary_key;auto_increment" json:"id"`
|
||||
TelegramID uint64 `gorm:"not null" json:"telegram_id"`
|
||||
TelegramID int64 `gorm:"not null" json:"telegram_id"`
|
||||
Chats []Chat `gorm:"foreignKey:UserID" json:"chats"`
|
||||
}
|
||||
|
||||
|
@ -15,17 +15,17 @@ func NewChat(db *gorm.DB) *Chat {
|
||||
}
|
||||
|
||||
func (c *Chat) ByID(id uint64) (*model.Chat, error) {
|
||||
var chat *model.Chat
|
||||
if err := c.db.First(&chat, id).Error; err != nil {
|
||||
var chat model.Chat
|
||||
if err := c.db.Model(&chat).First(&chat, id).Error; err != nil {
|
||||
return nil, util.HandleRecordNotFound(err)
|
||||
}
|
||||
return chat, nil
|
||||
return &chat, nil
|
||||
}
|
||||
|
||||
func (c *Chat) ByIDWithIntegrations(id uint64) (*model.Chat, error) {
|
||||
var chat *model.Chat
|
||||
if err := c.db.Preload("Integrations").First(&chat, id).Error; err != nil {
|
||||
var chat model.Chat
|
||||
if err := c.db.Model(&chat).Preload("Integrations").First(&chat, id).Error; err != nil {
|
||||
return nil, util.HandleRecordNotFound(err)
|
||||
}
|
||||
return chat, nil
|
||||
return &chat, nil
|
||||
}
|
||||
|
@ -15,17 +15,29 @@ func NewUser(db *gorm.DB) *User {
|
||||
}
|
||||
|
||||
func (u *User) ByID(id uint64) (*model.User, error) {
|
||||
var user *model.User
|
||||
if err := u.db.First(&user, id).Error; err != nil {
|
||||
var user model.User
|
||||
if err := u.db.Model(&user).First(&user, id).Error; err != nil {
|
||||
return nil, util.HandleRecordNotFound(err)
|
||||
}
|
||||
return user, nil
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (u *User) ByTelegramID(id int64) (*model.User, error) {
|
||||
var user model.User
|
||||
if err := u.db.Model(model.User{TelegramID: id}).First(&user).Error; err != nil {
|
||||
return nil, util.HandleRecordNotFound(err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (u *User) ByIDWithChats(id uint64) (*model.User, error) {
|
||||
var user *model.User
|
||||
if err := u.db.Preload("Chats").First(&user, id).Error; err != nil {
|
||||
var user model.User
|
||||
if err := u.db.Model(&user).Preload("Chats").First(&user, id).Error; err != nil {
|
||||
return nil, util.HandleRecordNotFound(err)
|
||||
}
|
||||
return user, nil
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (u *User) Save(user *model.User) error {
|
||||
return u.db.Model(&model.User{}).Save(user).Error
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||
"github.com/mymmrac/telego"
|
||||
)
|
||||
|
||||
type ChatMemberUpdatedHandler struct {
|
||||
app App
|
||||
app fsmcontract.App
|
||||
}
|
||||
|
||||
func NewChatMemberUpdatedHandler(app App) *ChatMemberUpdatedHandler {
|
||||
func NewChatMemberUpdatedHandler(app fsmcontract.App) *ChatMemberUpdatedHandler {
|
||||
return &ChatMemberUpdatedHandler{app: app}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package fsmcontract
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"github.com/mymmrac/telego"
|
||||
"go.uber.org/zap"
|
||||
@ -17,6 +16,6 @@ type App interface {
|
||||
Log() *zap.SugaredLogger
|
||||
TG() *telego.Bot
|
||||
Conf() *config.Config
|
||||
DB() *db.Repositories
|
||||
DB() Repositories
|
||||
Localizer(string) locale.Localizer
|
||||
}
|
||||
|
15
internal/handler/fsm/fsmcontract/db.go
Normal file
15
internal/handler/fsm/fsmcontract/db.go
Normal file
@ -0,0 +1,15 @@
|
||||
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{}
|
@ -24,14 +24,14 @@ const (
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
User uint64 `json:"u,omitempty"`
|
||||
Chat uint64 `json:"c,omitempty"`
|
||||
User int64 `json:"u,omitempty"`
|
||||
Chat int64 `json:"c,omitempty"`
|
||||
Action PayloadAction `json:"a"`
|
||||
Data json.RawMessage `json:"d,omitempty"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
ID uint64
|
||||
ID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ type Answer struct {
|
||||
Result bool `json:"r"`
|
||||
}
|
||||
|
||||
func NewNextPageMembersPayload(userID, chatID uint64) *Payload {
|
||||
func NewNextPageMembersPayload(userID, chatID int64) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionNext,
|
||||
User: userID,
|
||||
@ -52,7 +52,7 @@ func NewNextPageMembersPayload(userID, chatID uint64) *Payload {
|
||||
}
|
||||
}
|
||||
|
||||
func NewPrevPageMembersPayload(userID, chatID uint64) *Payload {
|
||||
func NewPrevPageMembersPayload(userID, chatID int64) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionPrevious,
|
||||
User: userID,
|
||||
@ -60,7 +60,7 @@ func NewPrevPageMembersPayload(userID, chatID uint64) *Payload {
|
||||
}
|
||||
}
|
||||
|
||||
func NewAddMemberPayload(userID, chatID, memberID uint64, memberName string) *Payload {
|
||||
func NewAddMemberPayload(userID, chatID, memberID int64, memberName string) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionAddMember,
|
||||
User: userID,
|
||||
@ -69,7 +69,7 @@ func NewAddMemberPayload(userID, chatID, memberID uint64, memberName string) *Pa
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoveMemberPayload(userID, chatID, memberID uint64, memberName string) *Payload {
|
||||
func NewRemoveMemberPayload(userID, chatID, memberID int64, memberName string) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionRemoveMember,
|
||||
User: userID,
|
||||
@ -78,7 +78,7 @@ func NewRemoveMemberPayload(userID, chatID, memberID uint64, memberName string)
|
||||
}
|
||||
}
|
||||
|
||||
func NewVotePayload(chatID uint64, vote float32) *Payload {
|
||||
func NewVotePayload(chatID int64, vote float32) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionVote,
|
||||
Chat: chatID,
|
||||
@ -86,7 +86,7 @@ func NewVotePayload(chatID uint64, vote float32) *Payload {
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfirmMembersPayload(userID uint64, chatID uint64) *Payload {
|
||||
func NewConfirmMembersPayload(userID int64, chatID int64) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionYesNo,
|
||||
User: userID,
|
||||
@ -95,7 +95,7 @@ func NewConfirmMembersPayload(userID uint64, chatID uint64) *Payload {
|
||||
}
|
||||
}
|
||||
|
||||
func NewRedmineQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payload {
|
||||
func NewRedmineQuestionPayload(userID int64, chatID int64, isYes bool) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionYesNo,
|
||||
User: userID,
|
||||
@ -104,7 +104,7 @@ func NewRedmineQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payloa
|
||||
}
|
||||
}
|
||||
|
||||
func NewRedmineHoursQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payload {
|
||||
func NewRedmineHoursQuestionPayload(userID int64, chatID int64, isYes bool) *Payload {
|
||||
return &Payload{
|
||||
Action: PayloadActionYesNo,
|
||||
User: userID,
|
||||
|
@ -25,11 +25,11 @@ type PollState struct {
|
||||
Result PollResult
|
||||
}
|
||||
|
||||
var Polls otter.Cache[uint64, PollState]
|
||||
var Polls otter.Cache[int64, PollState]
|
||||
|
||||
func init() {
|
||||
cache, err := otter.MustBuilder[uint64, PollState](10_000).
|
||||
Cost(func(key uint64, value PollState) uint32 {
|
||||
cache, err := otter.MustBuilder[int64, PollState](10_000).
|
||||
Cost(func(key int64, value PollState) uint32 {
|
||||
return 1
|
||||
}).
|
||||
WithTTL(time.Hour * 24 * 7).
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
userMachines = newStore[uint64, fsmcontract.Machine]()
|
||||
pollMachines = newStore[uint64, fsmcontract.Machine]()
|
||||
userMachines = newStore[int64, fsmcontract.Machine]()
|
||||
pollMachines = newStore[int64, fsmcontract.Machine]()
|
||||
)
|
||||
|
||||
type store[K comparable, V any] struct {
|
||||
@ -32,7 +32,7 @@ func (s *store[K, V]) Load(k K) (v V, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func ForUser(id uint64) fsmcontract.Machine {
|
||||
func ForUser(id int64) fsmcontract.Machine {
|
||||
val, ok := userMachines.Load(id)
|
||||
if !ok {
|
||||
machine := newUserMachine()
|
||||
@ -42,7 +42,7 @@ func ForUser(id uint64) fsmcontract.Machine {
|
||||
return val
|
||||
}
|
||||
|
||||
func ForPoll(id uint64) fsmcontract.Machine {
|
||||
func ForPoll(id int64) fsmcontract.Machine {
|
||||
val, ok := pollMachines.Load(id)
|
||||
if !ok {
|
||||
machine := newPollMachine()
|
||||
|
@ -1,11 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"github.com/mymmrac/telego"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Type uint8
|
||||
@ -16,14 +12,6 @@ const (
|
||||
ChatMemberUpdated
|
||||
)
|
||||
|
||||
type App interface {
|
||||
Log() *zap.SugaredLogger
|
||||
TG() *telego.Bot
|
||||
Conf() *config.Config
|
||||
DB() *db.Repositories
|
||||
Localizer(string) locale.Localizer
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
Handle(update telego.Update) error
|
||||
}
|
||||
|
28
internal/handler/iface/base.go
Normal file
28
internal/handler/iface/base.go
Normal file
@ -0,0 +1,28 @@
|
||||
package iface
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
App fsmcontract.App
|
||||
UserID int64
|
||||
ChatID int64
|
||||
}
|
||||
|
||||
func NewBase(app fsmcontract.App, userID int64, chatID int64) Base {
|
||||
return Base{app, userID, chatID}
|
||||
}
|
||||
|
||||
func (b Base) Localizer(lang string) locale.Localizer {
|
||||
if len(lang) > 2 {
|
||||
lang = lang[:2]
|
||||
}
|
||||
switch lang {
|
||||
case "en", "ru":
|
||||
return b.App.Localizer(lang)
|
||||
default:
|
||||
return b.App.Localizer("en")
|
||||
}
|
||||
}
|
@ -1,18 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/wizard"
|
||||
"github.com/mymmrac/telego"
|
||||
)
|
||||
|
||||
type MessageHandler struct {
|
||||
app App
|
||||
app fsmcontract.App
|
||||
}
|
||||
|
||||
func NewMessageHandler(app App) Handler {
|
||||
func NewMessageHandler(app fsmcontract.App) Handler {
|
||||
return &MessageHandler{app: app}
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Handle(telego.Update) error {
|
||||
// TODO implement me
|
||||
panic("implement me")
|
||||
func (h *MessageHandler) Handle(wh telego.Update) error {
|
||||
if wh.Message != nil {
|
||||
if wh.Message.From != nil &&
|
||||
wh.Message.Chat.Type == telego.ChatTypePrivate &&
|
||||
util.MatchCommand("start", wh.Message) {
|
||||
return wizard.NewRegister(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
||||
// TODO: Remove debug statement below.
|
||||
h.app.Log().Debugf("New Message: %s", func(msg *telego.Message) string {
|
||||
data, _ := json.Marshal(msg)
|
||||
return string(data)
|
||||
}(wh.Message))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
10
internal/handler/util/command.go
Normal file
10
internal/handler/util/command.go
Normal file
@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/mymmrac/telego"
|
||||
th "github.com/mymmrac/telego/telegohandler"
|
||||
)
|
||||
|
||||
func MatchCommand(command string, msg *telego.Message) bool {
|
||||
return th.CommandEqual(command)(telego.Update{Message: msg})
|
||||
}
|
54
internal/handler/wizard/register.go
Normal file
54
internal/handler/wizard/register.go
Normal file
@ -0,0 +1,54 @@
|
||||
package wizard
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
type Register struct {
|
||||
iface.Base
|
||||
}
|
||||
|
||||
func NewRegister(app fsmcontract.App, userID, chatID int64) *Register {
|
||||
return &Register{iface.NewBase(app, userID, chatID)}
|
||||
}
|
||||
|
||||
func (h *Register) Handle(wh telego.Update) error {
|
||||
loc := h.Localizer(wh.Message.From.LanguageCode)
|
||||
userRepo := h.App.DB().ForUser()
|
||||
user, err := userRepo.ByTelegramID(wh.Message.From.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user != nil && user.ID > 0 {
|
||||
_, err := h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: loc.Message("welcome"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
err = userRepo.Save(&model.User{
|
||||
TelegramID: wh.Message.From.ID,
|
||||
})
|
||||
if err != nil {
|
||||
_, _ = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: loc.Message("internal_error"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = h.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(wh.Message.Chat.ID),
|
||||
Text: loc.Message("welcome"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
})
|
||||
return err
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
welcome: "👋 Hello! This bot allows you to conduct Scrum Poker directly in a Telegram chat and pass results to issues tied to a Redmine instance. To start, add the bot to the chat where you want to conduct poker. After this, you can continue with the setup."
|
||||
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."
|
||||
previous: "◀️ Back"
|
||||
next: "Next ▶️"
|
||||
|
@ -1,4 +1,5 @@
|
||||
welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
|
||||
internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже."
|
||||
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере."
|
||||
previous: "◀️ Назад"
|
||||
next: "Вперед ▶️"
|
||||
|
Loading…
Reference in New Issue
Block a user