mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-27 12:16:41 +08:00
fix(tgbot): scope the add-client wizard to the admin, not the chat (#6604)
Two admins in one group chat shared one draft and one wizard step: clientDrafts and userStateStore were keyed by chat id alone, so the second admin's wizard opened on the first one's email and limits, and whichever of them tapped a control last decided what the other created. Key both stores by (chat, user) instead. A private chat is unaffected: its two ids are equal, so the key matches what the chat alone used to be. A message with no sender (a channel post) keys to user 0, which no admin holds. Fixes #6593.
This commit is contained in:
@@ -82,30 +82,52 @@ type clientDraft struct {
|
||||
reset int
|
||||
}
|
||||
|
||||
// clientDrafts keys a draft by chat: the steps arrive on the worker pool, so a
|
||||
// single draft let two admins fill in one client between them.
|
||||
type clientDrafts struct {
|
||||
mu sync.Mutex
|
||||
drafts map[int64]*clientDraft
|
||||
// chatUser names the admin a wizard belongs to. A private chat's ids are equal;
|
||||
// in a group they are not, and each admin at its keyboard fills in their own.
|
||||
type chatUser struct {
|
||||
chatID int64
|
||||
userID int64
|
||||
}
|
||||
|
||||
var addClientDrafts = &clientDrafts{drafts: make(map[int64]*clientDraft)}
|
||||
// messageActor reads the sender off a message. A post without one (a channel)
|
||||
// keys to user 0, an id no admin can hold.
|
||||
func messageActor(message telego.Message) chatUser {
|
||||
if message.From == nil {
|
||||
return chatUser{chatID: message.Chat.ID}
|
||||
}
|
||||
return chatUser{chatID: message.Chat.ID, userID: message.From.ID}
|
||||
}
|
||||
|
||||
func (s *clientDrafts) forChat(chatID int64) *clientDraft {
|
||||
// callbackActor reads the admin who tapped the button, not the chat the keyboard
|
||||
// sits in: every admin in a group sees the same keyboard.
|
||||
func callbackActor(callbackQuery *telego.CallbackQuery) chatUser {
|
||||
return chatUser{chatID: callbackQuery.Message.GetChat().ID, userID: callbackQuery.From.ID}
|
||||
}
|
||||
|
||||
// clientDrafts keys a draft by the admin filling it in: the steps arrive on the
|
||||
// worker pool, so one draft let two admins fill in one client between them.
|
||||
type clientDrafts struct {
|
||||
mu sync.Mutex
|
||||
drafts map[chatUser]*clientDraft
|
||||
}
|
||||
|
||||
var addClientDrafts = &clientDrafts{drafts: make(map[chatUser]*clientDraft)}
|
||||
|
||||
func (s *clientDrafts) forActor(actor chatUser) *clientDraft {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
draft, ok := s.drafts[chatID]
|
||||
draft, ok := s.drafts[actor]
|
||||
if !ok {
|
||||
draft = &clientDraft{}
|
||||
s.drafts[chatID] = draft
|
||||
s.drafts[actor] = draft
|
||||
}
|
||||
return draft
|
||||
}
|
||||
|
||||
func (s *clientDrafts) reset(chatID int64) {
|
||||
func (s *clientDrafts) reset(actor chatUser) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.drafts, chatID)
|
||||
delete(s.drafts, actor)
|
||||
}
|
||||
|
||||
// isAddClientStep reports whether callback data belongs to the add-client
|
||||
@@ -117,17 +139,17 @@ func isAddClientStep(data string) bool {
|
||||
func (s *clientDrafts) resetAll() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.drafts = make(map[int64]*clientDraft)
|
||||
s.drafts = make(map[chatUser]*clientDraft)
|
||||
}
|
||||
|
||||
// userStateStore guards the per-chat conversation states. The Telegram command
|
||||
// userStateStore guards the per-admin conversation states. The Telegram command
|
||||
// and callback handlers run on a worker-pool goroutine while the message handler
|
||||
// runs on the dispatch goroutine, so a bare map would be a concurrent-map-write
|
||||
// crash. It also expires abandoned conversations so a user who starts a flow and
|
||||
// goes silent doesn't leave an entry forever.
|
||||
type userStateStore struct {
|
||||
mu sync.Mutex
|
||||
states map[int64]userStateEntry
|
||||
states map[chatUser]userStateEntry
|
||||
lastPrune time.Time
|
||||
}
|
||||
|
||||
@@ -136,30 +158,30 @@ type userStateEntry struct {
|
||||
at time.Time
|
||||
}
|
||||
|
||||
var userStateMgr = &userStateStore{states: make(map[int64]userStateEntry)}
|
||||
var userStateMgr = &userStateStore{states: make(map[chatUser]userStateEntry)}
|
||||
|
||||
func (s *userStateStore) set(chatID int64, state string) {
|
||||
func (s *userStateStore) set(actor chatUser, state string) {
|
||||
s.mu.Lock()
|
||||
s.states[chatID] = userStateEntry{state: state, at: time.Now()}
|
||||
s.states[actor] = userStateEntry{state: state, at: time.Now()}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *userStateStore) get(chatID int64) (string, bool) {
|
||||
func (s *userStateStore) get(actor chatUser) (string, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
e, ok := s.states[chatID]
|
||||
e, ok := s.states[actor]
|
||||
return e.state, ok
|
||||
}
|
||||
|
||||
func (s *userStateStore) clear(chatID int64) {
|
||||
func (s *userStateStore) clear(actor chatUser) {
|
||||
s.mu.Lock()
|
||||
delete(s.states, chatID)
|
||||
delete(s.states, actor)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *userStateStore) reset() {
|
||||
s.mu.Lock()
|
||||
s.states = make(map[int64]userStateEntry)
|
||||
s.states = make(map[chatUser]userStateEntry)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ func TestAddClientExpiryPresetReplacesTheTerm(t *testing.T) {
|
||||
swapTestBot(t, url)
|
||||
|
||||
// A fresh draft attaches no inbound, so the card never looks one up by remark.
|
||||
draft := addClientDrafts.forChat(chatID)
|
||||
draft := addClientDrafts.forActor(chatUser{chatID: chatID, userID: 1})
|
||||
origRunning := isRunning
|
||||
t.Cleanup(func() {
|
||||
addClientDrafts.reset(chatID)
|
||||
addClientDrafts.reset(chatUser{chatID: chatID, userID: 1})
|
||||
isRunning = origRunning
|
||||
})
|
||||
isRunning = true
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
|
||||
"github.com/mymmrac/telego"
|
||||
)
|
||||
|
||||
// Regression test: keying the add-client wizard by chat alone left the two
|
||||
// admins of a group chat filling in one client between them.
|
||||
func TestAddClientDraftIsPerAdminInGroupChat(t *testing.T) {
|
||||
if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
|
||||
const (
|
||||
groupChat = int64(-1001234567890)
|
||||
adminA = int64(8101)
|
||||
adminB = int64(8202)
|
||||
)
|
||||
url, textsFor := draftTexts(t)
|
||||
swapTestBot(t, url)
|
||||
origRunning := isRunning
|
||||
t.Cleanup(func() {
|
||||
isRunning = origRunning
|
||||
userStateMgr.reset()
|
||||
})
|
||||
isRunning = true
|
||||
|
||||
// Both admins tap in the same chat; only the sender tells them apart.
|
||||
tap := func(userID int64, data string) {
|
||||
t.Helper()
|
||||
(&Tgbot{}).answerCallback(&telego.CallbackQuery{
|
||||
ID: "q1",
|
||||
From: telego.User{ID: userID},
|
||||
Data: data,
|
||||
Message: &telego.Message{MessageID: 7, Chat: telego.Chat{ID: groupChat}},
|
||||
}, true)
|
||||
}
|
||||
|
||||
tap(adminA, "add_client_to 1")
|
||||
emailA := cardEmail(t, lastDraftCard(t, textsFor(groupChat)))
|
||||
tap(adminB, "add_client_to 2")
|
||||
emailB := cardEmail(t, lastDraftCard(t, textsFor(groupChat)))
|
||||
if emailA == "" || emailA == emailB {
|
||||
t.Fatalf("both admins start with email %q, want one draft per admin", emailA)
|
||||
}
|
||||
|
||||
// Admin A renders again, with admin B's wizard already past its start.
|
||||
tap(adminA, "add_client_default_traffic_exp")
|
||||
if got := cardEmail(t, lastDraftCard(t, textsFor(groupChat))); got != emailA {
|
||||
t.Errorf("admin A's card shows email %q, want its own %q from admin B's draft", got, emailA)
|
||||
}
|
||||
|
||||
// The step they are each on is their own too: A's prompt must not put B into
|
||||
// the same step, or B's next message lands in A's wizard.
|
||||
tap(adminA, "add_client_ch_default_email")
|
||||
if st, ok := userStateMgr.get(chatUser{chatID: groupChat, userID: adminA}); !ok || st != "awaiting_email" {
|
||||
t.Errorf("admin A's step = %q (set: %v), want awaiting_email", st, ok)
|
||||
}
|
||||
if st, ok := userStateMgr.get(chatUser{chatID: groupChat, userID: adminB}); ok {
|
||||
t.Errorf("admin B's step = %q, want none: only the tapper's step may change", st)
|
||||
}
|
||||
}
|
||||
|
||||
// The wizard's typed steps arrive as messages, and that handler is a closure no
|
||||
// test can drive, so pin the key it takes there: sender, not chat.
|
||||
func TestMessageActorSeparatesAdminsInOneChat(t *testing.T) {
|
||||
const (
|
||||
groupChat = int64(-1001234567890)
|
||||
adminA = int64(8101)
|
||||
adminB = int64(8202)
|
||||
)
|
||||
t.Cleanup(userStateMgr.reset)
|
||||
|
||||
fromA := messageActor(telego.Message{Chat: telego.Chat{ID: groupChat}, From: &telego.User{ID: adminA}})
|
||||
fromB := messageActor(telego.Message{Chat: telego.Chat{ID: groupChat}, From: &telego.User{ID: adminB}})
|
||||
// A channel post carries no sender and must not land on an admin's step.
|
||||
fromChannel := messageActor(telego.Message{Chat: telego.Chat{ID: groupChat}})
|
||||
|
||||
if want := (chatUser{chatID: groupChat, userID: adminA}); fromA != want {
|
||||
t.Errorf("message from admin A keyed as %+v, want %+v", fromA, want)
|
||||
}
|
||||
if fromA == fromB || fromA == fromChannel || fromB == fromChannel {
|
||||
t.Fatalf("keys collide: %+v, %+v, %+v", fromA, fromB, fromChannel)
|
||||
}
|
||||
|
||||
userStateMgr.set(fromA, "awaiting_email")
|
||||
if st, ok := userStateMgr.get(fromB); ok {
|
||||
t.Errorf("admin B sees step %q: admin A's answer would land in B's wizard", st)
|
||||
}
|
||||
if st, ok := userStateMgr.get(fromChannel); ok {
|
||||
t.Errorf("a senderless post sees step %q", st)
|
||||
}
|
||||
if st, ok := userStateMgr.get(fromA); !ok || st != "awaiting_email" {
|
||||
t.Errorf("admin A's step = %q (set: %v), want awaiting_email", st, ok)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestNonWizardCallbackTakesNoDraftLock(t *testing.T) {
|
||||
)
|
||||
decliningServer(t)
|
||||
|
||||
held := addClientDrafts.forChat(heldChat)
|
||||
held := addClientDrafts.forActor(chatUser{chatID: heldChat, userID: 1})
|
||||
held.Lock()
|
||||
defer held.Unlock()
|
||||
|
||||
@@ -170,7 +170,7 @@ func TestNonWizardCallbackTakesNoDraftLock(t *testing.T) {
|
||||
tap(spareChat, false, "add_client_to 1")
|
||||
|
||||
addClientDrafts.mu.Lock()
|
||||
_, stored := addClientDrafts.drafts[spareChat]
|
||||
_, stored := addClientDrafts.drafts[chatUser{chatID: spareChat, userID: 1}]
|
||||
addClientDrafts.mu.Unlock()
|
||||
if stored {
|
||||
t.Errorf("draft stored for chat %d, want none until its wizard starts", spareChat)
|
||||
|
||||
@@ -7,12 +7,12 @@ func TestDeleteMessageAfterDelayKeepsUserState(t *testing.T) {
|
||||
t.Cleanup(userStateMgr.reset)
|
||||
|
||||
const chatID = int64(4242)
|
||||
userStateMgr.set(chatID, "awaiting_comment")
|
||||
userStateMgr.set(chatUser{chatID: chatID, userID: 1}, "awaiting_comment")
|
||||
|
||||
tg := &Tgbot{}
|
||||
tg.deleteMessageAfterDelay(chatID, 1, 0)
|
||||
|
||||
if st, ok := userStateMgr.get(chatID); !ok || st != "awaiting_comment" {
|
||||
if st, ok := userStateMgr.get(chatUser{chatID: chatID, userID: 1}); !ok || st != "awaiting_comment" {
|
||||
t.Fatalf("delayed message deletion cleared the conversation state: got (%q, %v), want (%q, true)", st, ok, "awaiting_comment")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ const clientDraftTestChatID = -9001
|
||||
// Regression test: the draft is sent with ParseMode HTML, so Markdown markers
|
||||
// were rendered literally and an unescaped value could break the whole message.
|
||||
func TestClientDraftMessageRendersHTML(t *testing.T) {
|
||||
draft := addClientDrafts.forChat(clientDraftTestChatID)
|
||||
t.Cleanup(func() { addClientDrafts.reset(clientDraftTestChatID) })
|
||||
draft := addClientDrafts.forActor(chatUser{chatID: clientDraftTestChatID, userID: 1})
|
||||
t.Cleanup(func() { addClientDrafts.reset(chatUser{chatID: clientDraftTestChatID, userID: 1}) })
|
||||
|
||||
draft.email = "a@b.c"
|
||||
draft.comment = "<b>promo</b> & <10 GB>"
|
||||
@@ -67,10 +67,10 @@ func TestAddClientPromptsEscapeDraftValues(t *testing.T) {
|
||||
url, texts := draftTexts(t)
|
||||
swapTestBot(t, url)
|
||||
|
||||
draft := addClientDrafts.forChat(1)
|
||||
draft := addClientDrafts.forActor(chatUser{chatID: 1, userID: 1})
|
||||
origRunning := isRunning
|
||||
t.Cleanup(func() {
|
||||
addClientDrafts.reset(1)
|
||||
addClientDrafts.reset(chatUser{chatID: 1, userID: 1})
|
||||
isRunning = origRunning
|
||||
})
|
||||
isRunning = true
|
||||
|
||||
@@ -82,7 +82,7 @@ func (t *Tgbot) OnReceive() {
|
||||
|
||||
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
defer recoverBotPanic()
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(messageActor(message))
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
|
||||
return nil
|
||||
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
||||
@@ -95,7 +95,7 @@ func (t *Tgbot) OnReceive() {
|
||||
|
||||
// Use goroutine with worker pool for concurrent command processing
|
||||
go runBotHandler(func() {
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(messageActor(message))
|
||||
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
|
||||
})
|
||||
return nil
|
||||
@@ -104,7 +104,7 @@ func (t *Tgbot) OnReceive() {
|
||||
h.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
||||
// Use goroutine with worker pool for concurrent callback processing
|
||||
go runBotHandler(func() {
|
||||
userStateMgr.clear(query.Message.GetChat().ID)
|
||||
userStateMgr.clear(callbackActor(&query))
|
||||
t.answerCallback(&query, checkAdmin(query.From.ID))
|
||||
})
|
||||
return nil
|
||||
@@ -113,22 +113,23 @@ func (t *Tgbot) OnReceive() {
|
||||
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
defer recoverBotPanic()
|
||||
userStateMgr.maybePrune(time.Hour)
|
||||
if userState, exists := userStateMgr.get(message.Chat.ID); exists {
|
||||
actor := messageActor(message)
|
||||
if userState, exists := userStateMgr.get(actor); exists {
|
||||
// Only a wizard step touches the draft, so only it takes the lock.
|
||||
draft := addClientDrafts.forChat(message.Chat.ID)
|
||||
draft := addClientDrafts.forActor(actor)
|
||||
draft.Lock()
|
||||
defer draft.Unlock()
|
||||
switch userState {
|
||||
case "awaiting_email":
|
||||
if draft.email == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(actor)
|
||||
return nil
|
||||
}
|
||||
|
||||
draft.email = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(draft.email) {
|
||||
userStateMgr.set(message.Chat.ID, "awaiting_email")
|
||||
userStateMgr.set(actor, "awaiting_email")
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -139,26 +140,26 @@ func (t *Tgbot) OnReceive() {
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(actor)
|
||||
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
||||
}
|
||||
case "awaiting_comment":
|
||||
if draft.comment == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(actor)
|
||||
return nil
|
||||
}
|
||||
|
||||
draft.comment = strings.TrimSpace(message.Text)
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(actor)
|
||||
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
||||
case "awaiting_tg_id":
|
||||
input := strings.TrimSpace(message.Text)
|
||||
if input == "" || input == "-" || strings.EqualFold(input, "none") {
|
||||
draft.tgID = ""
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(actor)
|
||||
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
||||
return nil
|
||||
}
|
||||
@@ -173,7 +174,7 @@ func (t *Tgbot) OnReceive() {
|
||||
}
|
||||
draft.tgID = input
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.userSaved"), 3, tu.ReplyKeyboardRemove())
|
||||
userStateMgr.clear(message.Chat.ID)
|
||||
userStateMgr.clear(actor)
|
||||
t.addClient(message.Chat.ID, draft, t.BuildClientDraftMessage(draft))
|
||||
}
|
||||
} else {
|
||||
@@ -315,12 +316,13 @@ func isCommandForBot(text string, username string) bool {
|
||||
// answerCallback processes callback queries from inline keyboards.
|
||||
func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
||||
chatId := callbackQuery.Message.GetChat().ID
|
||||
actor := callbackActor(callbackQuery)
|
||||
|
||||
// Only an admin's wizard callbacks touch a draft, so only they take its lock:
|
||||
// a report tap must not wait on a slot, a rejected chat must not be stored.
|
||||
var draft *clientDraft
|
||||
if isAdmin && isAddClientStep(callbackQuery.Data) {
|
||||
draft = addClientDrafts.forChat(chatId)
|
||||
draft = addClientDrafts.forActor(actor)
|
||||
draft.Lock()
|
||||
defer draft.Unlock()
|
||||
}
|
||||
@@ -1066,7 +1068,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds)
|
||||
case "add_client_ch_default_email":
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
userStateMgr.set(chatId, "awaiting_email")
|
||||
userStateMgr.set(actor, "awaiting_email")
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
@@ -1076,7 +1078,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
||||
case "add_client_ch_default_comment":
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
userStateMgr.set(chatId, "awaiting_comment")
|
||||
userStateMgr.set(actor, "awaiting_comment")
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
@@ -1086,7 +1088,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
||||
case "add_client_ch_default_tg_id":
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
userStateMgr.set(chatId, "awaiting_tg_id")
|
||||
userStateMgr.set(actor, "awaiting_tg_id")
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
@@ -1189,11 +1191,11 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
case "add_client_default_info":
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
userStateMgr.clear(chatId)
|
||||
userStateMgr.clear(actor)
|
||||
t.addClient(chatId, draft, t.BuildClientDraftMessage(draft))
|
||||
case "add_client_cancel":
|
||||
userStateMgr.clear(chatId)
|
||||
addClientDrafts.reset(chatId)
|
||||
userStateMgr.clear(actor)
|
||||
addClientDrafts.reset(actor)
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 3, tu.ReplyKeyboardRemove())
|
||||
case "add_client_default_traffic_exp":
|
||||
@@ -1235,7 +1237,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
||||
t.sendClientIndividualLinks(chatId, draft.email)
|
||||
t.sendClientQRLinks(chatId, draft.email)
|
||||
addClientDrafts.reset(chatId)
|
||||
addClientDrafts.reset(actor)
|
||||
}
|
||||
case "add_client_submit_enable":
|
||||
draft.enable = true
|
||||
@@ -1248,7 +1250,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
|
||||
t.sendClientIndividualLinks(chatId, draft.email)
|
||||
t.sendClientQRLinks(chatId, draft.email)
|
||||
addClientDrafts.reset(chatId)
|
||||
addClientDrafts.reset(actor)
|
||||
}
|
||||
case "reset_all_traffics_cancel":
|
||||
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
|
||||
|
||||
Reference in New Issue
Block a user