diff --git a/patches/0002-nuzzles.patch b/patches/0002-nuzzles.patch new file mode 100644 index 0000000..2c99613 --- /dev/null +++ b/patches/0002-nuzzles.patch @@ -0,0 +1,180 @@ +--- a/config/config.go ++++ b/config/config.go +@@ -8,14 +8,15 @@ + ) + + type Config struct { +- BotToken *string `yaml:"bot_token"` +- AdminChatId *int64 `yaml:"admin_chat_id"` +- AdminChatTopicId *int `yaml:"admin_chat_topic_id"` +- TargetChatId *int64 `yaml:"target_chat_id"` +- EntryMessage string `yaml:"entry_message"` +- ApprovalMessage string `yaml:"approval_message"` +- SendApprovalMessage bool `yaml:"send_approval_message"` +- DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"` ++ BotToken *string `yaml:"bot_token"` ++ AdminChatId *int64 `yaml:"admin_chat_id"` ++ AdminChatTopicId *int `yaml:"admin_chat_topic_id"` ++ TargetChatId *int64 `yaml:"target_chat_id"` ++ EntryMessage string `yaml:"entry_message"` ++ ApprovalMessage string `yaml:"approval_message"` ++ SendApprovalMessage bool `yaml:"send_approval_message"` ++ DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"` ++ CannedDeclineResponses []string `yaml:"canned_decline_responses"` + } + + func (c *Config) LoadConfig() error { +@@ -47,6 +48,7 @@ + ApprovalMessage: "", + SendApprovalMessage: false, + DeleteRequestAfterDecision: false, ++ CannedDeclineResponses: []string{}, + } + + encoder := yaml.NewEncoder(f) +--- a/handlers/callbacks.go ++++ b/handlers/callbacks.go +@@ -36,6 +36,7 @@ + switch action { + case "approve": + bot.handleApproveRequest(query, user, userString, adminUserString) ++ bot.DeletePendingUser(args) + case "decline": + bot.handleDeclineRequest(query, user, userString, adminUserString) + case "ban": +@@ -44,10 +45,19 @@ + 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 + } + +- bot.DeletePendingUser(args) +- + if bot.Config.DeleteRequestAfterDecision { + go bot.scheduleMessageDeletion(query.Message.Chat.ID, query.Message.MessageID, 10*time.Second) + } +@@ -93,14 +103,39 @@ + return + } + +- utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID, +- fmt.Sprintf(AdminDeclinedMsg, +- userString, user.From.ID, user.JoinReason, adminUserString, +- time.Now().Format("2006-01-02 15:04:05"), +- defaultReason, +- ), ++ 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.")) + } + +@@ -175,12 +210,14 @@ + 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[1:]) ++ 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)) + +@@ -188,6 +225,26 @@ + 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, "_"), " ") +--- a/handlers/handlers.go 2026-03-10 ++++ b/handlers/handlers.go 2026-03-11 + +@@ -13,7 +13,7 @@ + AdminDeclinedMsg = "❌ Join #request declined for %s [%d]\n\nJoin reason: %s\nDeclined by: %s\nDeclined at: %s\nDeclined reason: %s" + AdminBannedMsg = "🚫 Join #request banned for %s [%d]\n\nJoin reason: %s\nBanned by: %s\nBanned at: %s\nBanned until: %s" + AdminFailedMsg = "⚠️ Join #request failed for %s [%d]\n\nJoin reason: %s\nFailure reason: %s" +- defaultReason = "(no reason provided, reply to this to set one, prepend with + to also send to user)" ++ defaultReason = "(no reason provided, reply to this message to send one to the user, prepend with / to just set it)" + ) + + // Types shared by handler files +diff -ruN '--exclude=.git' '--exclude=.gitmodules' '--exclude=telegram-join-approval-bot' telegram-join-approval-bot/pkg/utils/utils.go telegram-join-approval-nuzzles/pkg/utils/utils.go +--- a/pkg/utils/utils.go 2026-03-10 ++++ b/pkg/utils/utils.go 2026-03-11 +@@ -38,7 +38,7 @@ + func NewApprovalKeyboard(userID int64) api.InlineKeyboardMarkup { + approveBtn := api.NewInlineKeyboardButtonData("Approve", fmt.Sprintf("approve_%d", userID)) + declineBtn := api.NewInlineKeyboardButtonData("Decline", fmt.Sprintf("decline_%d", userID)) +- banBtn := api.NewInlineKeyboardButtonData("Ban", fmt.Sprintf("ban_%d", userID)) ++ banBtn := api.NewInlineKeyboardButtonData("Ban (24h)", fmt.Sprintf("ban_%d", userID)) + return api.NewInlineKeyboardMarkup( + []api.InlineKeyboardButton{approveBtn, declineBtn}, + []api.InlineKeyboardButton{banBtn}, diff --git a/patches/0003-nuzzles.patch b/patches/0003-nuzzles.patch new file mode 100644 index 0000000..d40c0b7 --- /dev/null +++ b/patches/0003-nuzzles.patch @@ -0,0 +1,246 @@ +--- a/config/config.go ++++ b/config/config.go +@@ -8,14 +8,16 @@ + ) + + type Config struct { +- BotToken *string `yaml:"bot_token"` +- AdminChatId *int64 `yaml:"admin_chat_id"` +- AdminChatTopicId *int `yaml:"admin_chat_topic_id"` +- TargetChatId *int64 `yaml:"target_chat_id"` +- EntryMessage string `yaml:"entry_message"` +- ApprovalMessage string `yaml:"approval_message"` +- SendApprovalMessage bool `yaml:"send_approval_message"` +- DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"` ++ BotToken *string `yaml:"bot_token"` ++ AdminChatId *int64 `yaml:"admin_chat_id"` ++ AdminChatTopicId *int `yaml:"admin_chat_topic_id"` ++ TargetChatId *int64 `yaml:"target_chat_id"` ++ EntryMessage string `yaml:"entry_message"` ++ ApprovalMessage string `yaml:"approval_message"` ++ ReminderMessage string `yaml:"reminder_message"` ++ SendApprovalMessage bool `yaml:"send_approval_message"` ++ DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"` ++ CannedDeclineResponses []string `yaml:"canned_decline_responses"` + } + + func (c *Config) LoadConfig() error { +@@ -43,10 +45,12 @@ + AdminChatId: Int64Ptr(0), + AdminChatTopicId: IntPtr(0), + TargetChatId: Int64Ptr(0), +- EntryMessage: "You have requested to join the group, please write a brief message explaining why you want to join.", ++ EntryMessage: "You have requested to join the group. Please write a brief message explaining why you want to join.", ++ ReminderMessage: "Don't forget to give a one-message response to your join request.", + ApprovalMessage: "", + SendApprovalMessage: false, + DeleteRequestAfterDecision: false, ++ CannedDeclineResponses: []string{}, + } + + encoder := yaml.NewEncoder(f) +--- a/handlers/callbacks.go ++++ b//handlers/callbacks.go +@@ -39,7 +39,6 @@ + bot.DeletePendingUser(args) + case "decline": + bot.handleDeclineRequest(query, user, userString, adminUserString) +- bot.DeletePendingUser(args) + case "ban": + bot.showBanConfirmation(query, user) + bot.API.Request(api.NewCallback(query.ID, "")) +@@ -47,6 +46,19 @@ + case "banc": + bot.handleBanRequest(query, user, userString, adminUserString) + bot.DeletePendingUser(args) ++ case "remind": ++ bot.sendReminder(query, user, userString, adminUserString) ++ bot.API.Request(api.NewCallback(query.ID, "Reminder sent!")) ++ 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 { +@@ -54,6 +66,22 @@ + } + } + ++func (bot *Bot) sendReminder(query *api.CallbackQuery, user *ExtendedChatJoinRequest, userString, adminUserString string) { ++ utils.SendMessage(bot.API, user.From.ID, 0, bot.Config.ReminderMessage) ++ ++ // Edit admin message to show reminder was sent ++ messageText := fmt.Sprintf(AdminJoinRequestMsg, userString, user.From.ID, user.JoinReason) ++ messageText += fmt.Sprintf("\n\nReminder sent by: %s\nReminder sent at: %s", ++ adminUserString, time.Now().Format("2006-01-02 15:04:05")) ++ ++ keyboard := utils.NewApprovalKeyboard(user.From.ID) ++ ++ edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, messageText) ++ edit.ParseMode = api.ModeHTML ++ edit.ReplyMarkup = &keyboard ++ bot.API.Send(edit) ++} ++ + // 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{ +@@ -94,14 +122,39 @@ + return + } + +- utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID, +- fmt.Sprintf(AdminDeclinedMsg, +- userString, user.From.ID, user.JoinReason, adminUserString, +- time.Now().Format("2006-01-02 15:04:05"), +- defaultReason, +- ), ++ 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.")) + } + +@@ -176,12 +229,14 @@ + 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[1:]) ++ 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)) + +@@ -189,6 +244,26 @@ + 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, "_"), " ") + +--- a/handlers/handlers.go ++++ b/handlers/handlers.go +@@ -13,7 +13,7 @@ + AdminDeclinedMsg = "❌ Join #request declined for %s [%d]\n\nJoin reason: %s\nDeclined by: %s\nDeclined at: %s\nDeclined reason: %s" + AdminBannedMsg = "🚫 Join #request banned for %s [%d]\n\nJoin reason: %s\nBanned by: %s\nBanned at: %s\nBanned until: %s" + AdminFailedMsg = "⚠️ Join #request failed for %s [%d]\n\nJoin reason: %s\nFailure reason: %s" +- defaultReason = "(no reason provided, reply to this to set one, prepend with + to also send to user)" ++ defaultReason = "(no reason provided, reply to this message to send one to the user, prepend with / to just set it)" + ) + + // Types shared by handler files +--- a/handlers/join.go ++++ b/handlers/join.go +@@ -19,6 +19,10 @@ + userString := utils.BuildUserString(&user.From) + + keyboard := utils.NewApprovalKeyboard(user.From.ID) ++ if bot.Config.ReminderMessage != "" { ++ newButton := api.NewInlineKeyboardButtonData("Send Reminder", fmt.Sprintf("remind_%d", user.From.ID)) ++ keyboard.InlineKeyboard[1] = append([]api.InlineKeyboardButton{newButton}, keyboard.InlineKeyboard[1]...) ++ } + utils.EditMessageWithKeyboard(bot.API, *bot.Config.AdminChatId, user.JoinRequestMessageID, + fmt.Sprintf(AdminJoinRequestMsg, userString, user.From.ID, user.JoinReason), &keyboard) + +@@ -27,12 +31,24 @@ + + // HandleJoinRequest initiates the join approval flow by sending the entry message and admin notification. + func (bot *Bot) HandleJoinRequest(request *api.ChatJoinRequest) { ++ // Check if user already has a pending request ++ if existingUser := bot.GetPendingUser(request.From.ID); existingUser != nil { ++ utils.SendMessage(bot.API, request.From.ID, 0, ++ "You have already requested to join. Please send a single message as your join reason.") ++ return ++ } ++ + utils.SendMessage(bot.API, request.From.ID, 0, bot.Config.EntryMessage) + userString := utils.BuildUserString(&request.From) + ++ keyboard := utils.NewApprovalKeyboard(request.From.ID) ++ if bot.Config.ReminderMessage != "" { ++ newButton := api.NewInlineKeyboardButtonData("Send Reminder", fmt.Sprintf("remind_%d", request.From.ID)) ++ keyboard.InlineKeyboard[1] = append([]api.InlineKeyboardButton{newButton}, keyboard.InlineKeyboard[1]...) ++ } + m := api.NewMessage(*bot.Config.AdminChatId, + fmt.Sprintf(AdminJoinRequestMsg, userString, request.From.ID, "(awaiting user response)")) +- m.ReplyMarkup = utils.NewApprovalKeyboard(request.From.ID) ++ m.ReplyMarkup = keyboard + m.ParseMode = api.ModeHTML + m.LinkPreviewOptions = api.LinkPreviewOptions{IsDisabled: true} + if topic := *bot.Config.AdminChatTopicId; topic != 0 { +--- a/pkg/utils/utils.go ++++ b/pkg/utils/utils.go +@@ -38,7 +38,7 @@ + func NewApprovalKeyboard(userID int64) api.InlineKeyboardMarkup { + approveBtn := api.NewInlineKeyboardButtonData("Approve", fmt.Sprintf("approve_%d", userID)) + declineBtn := api.NewInlineKeyboardButtonData("Decline", fmt.Sprintf("decline_%d", userID)) +- banBtn := api.NewInlineKeyboardButtonData("Ban (24h)", fmt.Sprintf("ban_%d", userID)) ++ banBtn := api.NewInlineKeyboardButtonData("⚠️ Ban (24h)aaa", fmt.Sprintf("ban_%d", userID)) + return api.NewInlineKeyboardMarkup( + []api.InlineKeyboardButton{approveBtn, declineBtn}, + []api.InlineKeyboardButton{banBtn}, diff --git a/patches/0004-callbacks.go.patch b/patches/0004-callbacks.go.patch new file mode 100644 index 0000000..d71db9e --- /dev/null +++ b/patches/0004-callbacks.go.patch @@ -0,0 +1,152 @@ +--- a/handlers/callbacks.go ++++ b//handlers/callbacks.go +@@ -6,7 +6,7 @@ + "strings" + "time" + +- utils "git.zio.sh/astra/telegram-join-approval-bot/pkg/utils" ++ utils "git.zio.sh/astra/telegram-join-approval-nuzzles/pkg/utils" + api "github.com/OvyFlash/telegram-bot-api" + ) + +@@ -39,7 +39,6 @@ + bot.DeletePendingUser(args) + case "decline": + bot.handleDeclineRequest(query, user, userString, adminUserString) +- bot.DeletePendingUser(args) + case "ban": + bot.showBanConfirmation(query, user) + bot.API.Request(api.NewCallback(query.ID, "")) +@@ -47,6 +46,19 @@ + case "banc": + bot.handleBanRequest(query, user, userString, adminUserString) + bot.DeletePendingUser(args) ++ case "remind": ++ bot.sendReminder(query, user, userString, adminUserString) ++ bot.API.Request(api.NewCallback(query.ID, "Reminder sent!")) ++ 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 { +@@ -54,6 +66,22 @@ + } + } + ++func (bot *Bot) sendReminder(query *api.CallbackQuery, user *ExtendedChatJoinRequest, userString, adminUserString string) { ++ utils.SendMessage(bot.API, user.From.ID, 0, bot.Config.ReminderMessage) ++ ++ // Edit admin message to show reminder was sent ++ messageText := fmt.Sprintf(AdminJoinRequestMsg, userString, user.From.ID, user.JoinReason) ++ messageText += fmt.Sprintf("\n\nReminder sent by: %s\nReminder sent at: %s", ++ adminUserString, time.Now().Format("2006-01-02 15:04:05")) ++ ++ keyboard := utils.NewApprovalKeyboard(user.From.ID) ++ ++ edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, messageText) ++ edit.ParseMode = api.ModeHTML ++ edit.ReplyMarkup = &keyboard ++ bot.API.Send(edit) ++} ++ + // 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{ +@@ -94,14 +122,39 @@ + return + } + +- utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID, +- fmt.Sprintf(AdminDeclinedMsg, +- userString, user.From.ID, user.JoinReason, adminUserString, +- time.Now().Format("2006-01-02 15:04:05"), +- defaultReason, +- ), ++ 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.")) + } + +@@ -176,12 +229,14 @@ + 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[1:]) ++ 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)) + +@@ -189,6 +244,26 @@ + 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, "_"), " ") diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index c18a452..e0b4ebe 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -38,7 +38,7 @@ func EscapeHTML(s string) string { func NewApprovalKeyboard(userID int64) api.InlineKeyboardMarkup { approveBtn := api.NewInlineKeyboardButtonData("Approve", fmt.Sprintf("approve_%d", userID)) declineBtn := api.NewInlineKeyboardButtonData("Decline", fmt.Sprintf("decline_%d", userID)) - banBtn := api.NewInlineKeyboardButtonData("⚠️ Ban (24h)", fmt.Sprintf("ban_%d", userID)) + banBtn := api.NewInlineKeyboardButtonData("⚠️ Ban (24h)aaa", fmt.Sprintf("ban_%d", userID)) return api.NewInlineKeyboardMarkup( []api.InlineKeyboardButton{approveBtn, declineBtn}, []api.InlineKeyboardButton{banBtn}, diff --git a/scripts/sync.sh b/scripts/sync.sh index ffcc7ae..77a31c2 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -15,7 +15,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" SUBMODULE_DIR="$ROOT_DIR/telegram-join-approval-bot" -PATCH_FILE="$ROOT_DIR/patches/0001-nuzzles.patch" +PATCH_FILE="$ROOT_DIR/patches/0003-nuzzles.patch" # ── 1. Ensure submodule is initialised ──────────────────────────────────────── echo "→ Updating submodule..." diff --git a/telegram-join-approval-bot b/telegram-join-approval-bot index 98e6a4a..f3d290d 160000 --- a/telegram-join-approval-bot +++ b/telegram-join-approval-bot @@ -1 +1 @@ -Subproject commit 98e6a4a100cb350cadc0a074894bb2fb0b0c9394 +Subproject commit f3d290d6d76cc03228d870bef9bdef7297e647f1 diff --git a/telegram-join-approval-nuzzles b/telegram-join-approval-nuzzles new file mode 160000 index 0000000..334fe2b --- /dev/null +++ b/telegram-join-approval-nuzzles @@ -0,0 +1 @@ +Subproject commit 334fe2bf8f84228ef0e0bfabb0fd4173a29ebb62