package handlers import ( "fmt" "log" "strings" "time" utils "git.zio.sh/astra/telegram-approval-join-nuzzles/pkg/utils" api "github.com/OvyFlash/telegram-bot-api" ) // HandleCallbackQuery processes inline button callbacks (approve/decline/ban/leave). func (bot *Bot) HandleCallbackQuery(query *api.CallbackQuery) { action, args, err := parseCallbackData(query.Data) if err != nil { log.Printf("Failed to parse callback data: %v", err) return } if action == "leave" { bot.handleLeaveAction(query, args) return } user := bot.GetPendingUser(args) if user == nil { log.Printf("No pending request for user ID %d", args) bot.handleMissingUser(query) return } userString := utils.BuildUserString(&user.From) adminUserString := utils.BuildUserString(query.From) switch action { case "approve": bot.handleApproveRequest(query, user, userString, adminUserString) bot.DeletePendingUser(args) case "decline": bot.handleDeclineRequest(query, user, userString, adminUserString) case "ban": bot.showBanConfirmation(query, user) bot.API.Request(api.NewCallback(query.ID, "")) return case "banc": bot.handleBanRequest(query, user, userString, adminUserString) bot.DeletePendingUser(args) case "cannedrespsel": parts := strings.Split(query.Data, "_") if len(parts) >= 3 { var respIdx int fmt.Sscanf(parts[2], "%d", &respIdx) bot.sendCannedResponse(query, user, respIdx) } bot.DeletePendingUser(args) bot.API.Request(api.NewCallback(query.ID, "")) return } if bot.Config.DeleteRequestAfterDecision { go bot.scheduleMessageDeletion(query.Message.Chat.ID, query.Message.MessageID, 10*time.Second) } } // handleApproveRequest approves a join request and sends an approval callback. func (bot *Bot) handleApproveRequest(query *api.CallbackQuery, user *ExtendedChatJoinRequest, userString, adminUserString string) { r := api.ApproveChatJoinRequestConfig{ ChatConfig: api.ChatConfig{ChatID: user.ChatJoinRequest.Chat.ID}, UserID: user.ChatJoinRequest.From.ID, } if _, err := bot.API.Request(r); err != nil { log.Println(err) bot.restoreMessage(query) return } utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID, fmt.Sprintf(AdminApprovedMsg, userString, user.From.ID, user.JoinReason, adminUserString, time.Now().Format("2006-01-02 15:04:05"), ), ) if bot.Config.SendApprovalMessage { utils.SendMessage(bot.API, user.From.ID, 0, bot.Config.ApprovalMessage) } bot.API.Request(api.NewCallback(query.ID, "Join request approved.")) } // handleDeclineRequest declines a join request and sends a decline callback. func (bot *Bot) handleDeclineRequest(query *api.CallbackQuery, user *ExtendedChatJoinRequest, userString, adminUserString string) { r := api.DeclineChatJoinRequest{ ChatConfig: api.ChatConfig{ChatID: user.ChatJoinRequest.Chat.ID}, UserID: user.ChatJoinRequest.From.ID, } if _, err := bot.API.Request(r); err != nil { log.Println(err) bot.restoreMessage(query) return } messageText := fmt.Sprintf(AdminDeclinedMsg, userString, user.From.ID, user.JoinReason, adminUserString, time.Now().Format("2006-01-02 15:04:05"), defaultReason, ) edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, messageText) edit.ParseMode = api.ModeHTML edit.Entities = query.Message.Entities if len(bot.Config.CannedDeclineResponses) > 0 { var rows [][]api.InlineKeyboardButton for i, response := range bot.Config.CannedDeclineResponses { // Clean up the response text for button display snippet := strings.TrimSpace(response) snippet = strings.ReplaceAll(snippet, "\n", " ") // Remove multiple consecutive spaces for strings.Contains(snippet, " ") { snippet = strings.ReplaceAll(snippet, " ", " ") } // Truncate to 30 chars for button text if len(snippet) > 30 { snippet = snippet[:30] + "..." } btn := api.NewInlineKeyboardButtonData(snippet, fmt.Sprintf("cannedrespsel_%d_%d", user.From.ID, i)) rows = append(rows, []api.InlineKeyboardButton{btn}) } keyboard := api.NewInlineKeyboardMarkup(rows...) edit.ReplyMarkup = &keyboard } bot.API.Send(edit) bot.API.Request(api.NewCallback(query.ID, "Join request declined.")) } // handleBanRequest bans a user from the chat for 24 hours. func (bot *Bot) handleBanRequest(query *api.CallbackQuery, user *ExtendedChatJoinRequest, userString, adminUserString string) { r := api.BanChatMemberConfig{ ChatMemberConfig: api.ChatMemberConfig{ ChatConfig: api.ChatConfig{ChatID: user.ChatJoinRequest.Chat.ID}, UserID: user.ChatJoinRequest.From.ID, }, UntilDate: time.Now().Add(24 * time.Hour).Unix(), } if _, err := bot.API.Request(r); err != nil { log.Println(err) bot.restoreMessage(query) return } declineRequest := api.DeclineChatJoinRequest{ ChatConfig: api.ChatConfig{ChatID: user.ChatJoinRequest.Chat.ID}, UserID: user.ChatJoinRequest.From.ID, } if _, err := bot.API.Request(declineRequest); err != nil { log.Println(err) bot.restoreMessage(query) return } bannedUntil := time.Now().Add(24 * time.Hour).Format("2006-01-02 15:04:05") utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID, fmt.Sprintf(AdminBannedMsg, userString, user.From.ID, user.JoinReason, adminUserString, time.Now().Format("2006-01-02 15:04:05"), bannedUntil, ), ) bot.API.Request(api.NewCallback(query.ID, "User banned.")) } // showBanConfirmation displays a confirmation prompt for the ban action. func (bot *Bot) showBanConfirmation(query *api.CallbackQuery, user *ExtendedChatJoinRequest) { approveBtn := api.NewInlineKeyboardButtonData("Approve", fmt.Sprintf("approve_%d", user.From.ID)) declineBtn := api.NewInlineKeyboardButtonData("Decline", fmt.Sprintf("decline_%d", user.From.ID)) confirmBtn := api.NewInlineKeyboardButtonData("Ban (confirm)", fmt.Sprintf("banc_%d", user.From.ID)) keyboard := api.NewInlineKeyboardMarkup( []api.InlineKeyboardButton{approveBtn, declineBtn}, []api.InlineKeyboardButton{confirmBtn}, ) edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, query.Message.Text) edit.ReplyMarkup = &keyboard edit.ParseMode = api.ModeHTML edit.Entities = query.Message.Entities bot.API.Send(edit) } // HandleDeclineReason allows admins to provide a decline reason by replying to decline messages. func (bot *Bot) HandleDeclineReason(update *api.Update) { repliedMsg := update.Message.ReplyToMessage if !strings.Contains(repliedMsg.Text, defaultReason) { return } lines := strings.Split(repliedMsg.Text, "\n") userString := utils.BuildUserString(update.Message.From) if strings.TrimPrefix(lines[3], "Declined by: ") != userString { return } userID, username, joinReason, declinedBy, declinedAt := utils.GetInfoFromMsg(repliedMsg.Text) reason := utils.EscapeHTML(update.Message.Text) if !strings.HasPrefix(update.Message.Text, "/") { reason = utils.EscapeHTML(update.Message.Text) utils.SendMessage(bot.API, userID, 0, fmt.Sprintf("Your join request was declined for the following reason:\n\n%s", reason)) } reason = strings.TrimPrefix(reason, "/") utils.EditMessage(bot.API, update.Message.Chat.ID, repliedMsg.MessageID, fmt.Sprintf(AdminDeclinedMsg, username, userID, joinReason, declinedBy, declinedAt, reason)) // Delete the admin's message after processing bot.API.Send(api.NewDeleteMessage(update.Message.Chat.ID, update.Message.MessageID)) } // sendCannedResponse sends a canned decline response to the declined user. func (bot *Bot) sendCannedResponse(query *api.CallbackQuery, user *ExtendedChatJoinRequest, respIdx int) { if respIdx < 0 || respIdx >= len(bot.Config.CannedDeclineResponses) { return } reason := utils.EscapeHTML(bot.Config.CannedDeclineResponses[respIdx]) utils.SendMessage(bot.API, user.From.ID, 0, fmt.Sprintf("Your join request was declined for the following reason:\n\n%s", reason)) // Extract user info from original message and reformat with the canned response userID, username, joinReason, declinedBy, declinedAt := utils.GetInfoFromMsg(query.Message.Text) edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, fmt.Sprintf(AdminDeclinedMsg, username, userID, joinReason, declinedBy, declinedAt, reason)) edit.ParseMode = api.ModeHTML edit.Entities = query.Message.Entities bot.API.Send(edit) } // parseCallbackData parses the action and user ID from a callback query's data string. func parseCallbackData(data string) (action string, userID int64, err error) { normalized := strings.Join(strings.Split(data, "_"), " ") _, err = fmt.Sscanf(normalized, "%s %d", &action, &userID) return } // handleLeaveAction handles the "leave" callback by leaving the specified chat. func (bot *Bot) handleLeaveAction(query *api.CallbackQuery, chatID int64) { utils.LeaveChatRequest(bot.API, []int64{chatID}) utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID, fmt.Sprintf("We have left chat %d", chatID)) bot.API.Request(api.NewCallback(query.ID, "")) } // handleMissingUser notifies the admin that the user's pending request could not be found. func (bot *Bot) handleMissingUser(query *api.CallbackQuery) { msg := api.NewMessage(query.Message.Chat.ID, "Unable to find user, bot may have restarted") msg.ReplyParameters = api.ReplyParameters{ MessageID: query.Message.MessageID, ChatID: query.Message.Chat.ID, } r, _ := bot.API.Send(msg) edit := api.NewEditMessageText(query.Message.Chat.ID, r.ReplyToMessage.MessageID, r.ReplyToMessage.Text) edit.Entities = r.ReplyToMessage.Entities bot.API.Send(edit) bot.API.Request(api.NewCallback(query.ID, "")) } // restoreMessage restores a message to its original state after a failed API request. func (bot *Bot) restoreMessage(query *api.CallbackQuery) { edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, query.Message.Text) edit.Entities = query.Message.Entities bot.API.Send(edit) } // scheduleMessageDeletion deletes a message after a given delay. func (bot *Bot) scheduleMessageDeletion(chatID int64, messageID int, delay time.Duration) { timer := time.NewTimer(delay) defer timer.Stop() <-timer.C bot.API.Send(api.NewDeleteMessage(chatID, messageID)) }