From d2f7297d92027cdf13e8eafecf38bc715add40ca Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 16 Feb 2026 16:49:20 +0000 Subject: [PATCH] Code refactor --- config.go | 52 -------- config/config.go | 62 +++++++++ handlers/callbacks.go | 159 +++++++++++++++++++++++ handlers/handlers.go | 60 +++++++++ handlers/join.go | 72 +++++++++++ main.go | 293 +++--------------------------------------- pkg/utils/utils.go | 166 ++++++++++++++++++++++++ 7 files changed, 534 insertions(+), 330 deletions(-) delete mode 100644 config.go create mode 100644 config/config.go create mode 100644 handlers/callbacks.go create mode 100644 handlers/handlers.go create mode 100644 handlers/join.go create mode 100644 pkg/utils/utils.go diff --git a/config.go b/config.go deleted file mode 100644 index 83eb279..0000000 --- a/config.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "go.yaml.in/yaml/v3" -) - -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"` - DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"` -} - -func (c *Config) LoadConfig() error { - f, err := os.Open("config.yaml") - if err != nil { - c.CreateConfig() - return fmt.Errorf("config.yaml not found, a new one has been created. Please fill it out and restart the bot") - } - defer f.Close() - - decoder := yaml.NewDecoder(f) - err = decoder.Decode(c) - return err -} - -func (c *Config) CreateConfig() error { - f, err := os.Create("config.yaml") - if err != nil { - return err - } - defer f.Close() - - defaultConfig := Config{ - BotToken: "YOUR_BOT_TOKEN_HERE", - AdminChatId: 0, - TargetChatId: 0, - EntryMessage: "You have requested to join the group, please write a brief message explaining why you want to join.", - ApprovalMessage: "", - DeleteRequestAfterDecision: false, - } - - encoder := yaml.NewEncoder(f) - err = encoder.Encode(defaultConfig) - return err -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..83d04c2 --- /dev/null +++ b/config/config.go @@ -0,0 +1,62 @@ +package config + +import ( + "fmt" + "os" + + "go.yaml.in/yaml/v3" +) + +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"` + DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"` +} + +func (c *Config) LoadConfig() error { + f, err := os.Open("config.yaml") + if err != nil { + c.CreateConfig() + return fmt.Errorf("config.yaml not found, a new one has been created. Please fill it out and restart the bot") + } + defer f.Close() + + decoder := yaml.NewDecoder(f) + err = decoder.Decode(c) + return err +} + +func (c *Config) CreateConfig() error { + f, err := os.Create("config.yaml") + if err != nil { + return err + } + defer f.Close() + + defaultConfig := Config{ + BotToken: StringPtr("YOUR_BOT_TOKEN_HERE"), + 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.", + ApprovalMessage: "", + DeleteRequestAfterDecision: false, + } + + encoder := yaml.NewEncoder(f) + err = encoder.Encode(defaultConfig) + return err +} + +// StringPtr returns a pointer to the given string. +func StringPtr(s string) *string { return &s } + +// Int64Ptr returns a pointer to the given int64. +func Int64Ptr(i int64) *int64 { return &i } + +// IntPtr returns a pointer to the given int. +func IntPtr(i int) *int { return &i } diff --git a/handlers/callbacks.go b/handlers/callbacks.go new file mode 100644 index 0000000..85553c4 --- /dev/null +++ b/handlers/callbacks.go @@ -0,0 +1,159 @@ +package handlers + +import ( + "fmt" + "log" + "strings" + "time" + + utils "git.zio.sh/astra/telegram-approval-join/pkg/utils" + api "github.com/OvyFlash/telegram-bot-api" +) + +// HandleCallbackQuery processes inline button callbacks (approve/decline/leave). +func (bot *Bot) HandleCallbackQuery(query *api.CallbackQuery) { + data := strings.Join(strings.Split(query.Data, "_"), " ") + var action string + var args int64 + _, err := fmt.Sscanf(data, "%s %d", &action, &args) + if err != nil { + log.Printf("Failed to parse callback data: %v", err) + return + } + + if action == "leave" { + utils.LeaveChatRequest(bot.API, []int64{args}) + utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID, + fmt.Sprintf("We have left chat %d", args)) + callback := api.NewCallback(query.ID, "") + bot.API.Request(callback) + return + } + + user := bot.GetPendingUser(args) + if user == nil { + log.Printf("No pending request for user ID %d", args) + 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) + + callback := api.NewCallback(query.ID, "") + bot.API.Request(callback) + return + } + + userString := utils.BuildUserString(&user.From) + adminUserString := utils.BuildUserString(query.From) + + switch action { + case "approve": + bot.handleApproveRequest(query, user, userString, adminUserString) + case "decline": + bot.handleDeclineRequest(query, user, userString, adminUserString) + } + + if bot.Config.DeleteRequestAfterDecision { + deleteTimer := time.NewTimer(10 * time.Second) + go func() { + defer deleteTimer.Stop() + <-deleteTimer.C + del := api.NewDeleteMessage(query.Message.Chat.ID, query.Message.MessageID) + bot.API.Send(del) + }() + } + + bot.DeletePendingUser(args) +} + +// handleApproveRequest approves a join request and sends 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, + } + _, e := bot.API.Request(r) + if e != nil { + log.Println(e.Error()) + edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, query.Message.Text) + edit.Entities = query.Message.Entities + bot.API.Send(edit) + 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.ApprovalMessage != "" { + utils.SendMessage(bot.API, user.From.ID, 0, bot.Config.ApprovalMessage) + } + + callback := api.NewCallback(query.ID, "Join request approved.") + bot.API.Request(callback) +} + +// handleDeclineRequest declines a join request and sends 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, + } + _, e := bot.API.Request(r) + if e != nil { + log.Println(e.Error()) + edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, query.Message.Text) + edit.Entities = query.Message.Entities + bot.API.Send(edit) + 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"), + "(no reason provided, reply to this to set one, prepend with + to also send to user)"), + ) + + callback := api.NewCallback(query.ID, "Join request declined.") + bot.API.Request(callback) +} + +// 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, + "(no reason provided, reply to this to set one, prepend with + to also send to user)") { + return + } + + lines := strings.Split(repliedMsg.Text, "\n") + userString := utils.BuildUserString(update.Message.From) + + if strings.TrimPrefix(lines[3], "Declined by: ") != userString { + return + } + + reason := utils.EscapeHTML(update.Message.Text) + userID, username, joinReason, declinedBy, declinedAt := utils.GetInfoFromMsg(repliedMsg.Text) + entities, _ := utils.FilterEntitiesByTypeWithContext(repliedMsg, "italic") + if len(entities) >= 1 { + username = fmt.Sprintf("%s", entities[0].Text) + } + + if strings.HasPrefix(update.Message.Text, "+") { + reason = utils.EscapeHTML(update.Message.Text[1:]) + utils.SendMessage(bot.API, userID, 0, + fmt.Sprintf("Your join request was declined for the following reason:\n\n%s", reason)) + } + + utils.EditMessage(bot.API, update.Message.Chat.ID, repliedMsg.MessageID, + fmt.Sprintf(AdminDeclinedMsg, username, userID, joinReason, declinedBy, declinedAt, reason)) +} diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..e3b14dc --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "sync" + + config "git.zio.sh/astra/telegram-approval-join/config" + api "github.com/OvyFlash/telegram-bot-api" +) + +const ( + AdminJoinRequestMsg = "New join #request from %s [%d]\n\nJoin reason: %s" + AdminApprovedMsg = "✅ Join #request approved for %s [%d]\n\nJoin reason: %s\nApproved by: %s\nApproved at: %s" + AdminDeclinedMsg = "❌ Join #request declined for %s [%d]\n\nJoin reason: %s\nDeclined by: %s\nDeclined at: %s\nDeclined reason: %s" + AdminFailedMsg = "⚠️ Join #request failed for %s [%d]\n\nJoin reason: %s\nFailure reason: %s" + BotApprovalEnabled = "Join approval bot enabled, to get started, add the bot to the target group and make it an admin with invite permissions, then use /targetchat <chat_id> to add the group to the config.\n\nTo set the entry message that users will receive when they request to join, use /setentrymessage <entry_message> in the admin chat. You can use HTML formatting in the entry message." + BotAddedToGroup = "Hello! I help out with join approvals.\n\nTo get started, make sure this bot is added to the admin group where it will send join approvals to be accepted or declined, then type /request when you have done that." + BotRequestMsg = "You have requested this chat to be the admin chat, please wait for it to be activated." +) + +// Types shared by handler files +type ExtendedChatJoinRequest struct { + *api.ChatJoinRequest + JoinReason string + JoinRequestMessageID int +} + +type Bot struct { + API *api.BotAPI + Config config.Config + mu sync.RWMutex + WaitingForApproval map[int64]*ExtendedChatJoinRequest +} + +// GetPendingUser retrieves a pending user request (read-safe). +func (bot *Bot) GetPendingUser(userID int64) *ExtendedChatJoinRequest { + bot.mu.RLock() + defer bot.mu.RUnlock() + user := bot.WaitingForApproval[userID] + if user == nil { + return nil + } + return user +} + +// SetPendingUser stores a pending user request (write-safe). +func (bot *Bot) SetPendingUser(userID int64, user *ExtendedChatJoinRequest) { + bot.mu.Lock() + defer bot.mu.Unlock() + if _, ok := bot.WaitingForApproval[userID]; !ok { + bot.WaitingForApproval = make(map[int64]*ExtendedChatJoinRequest) + } + bot.WaitingForApproval[userID] = user +} + +// DeletePendingUser removes a pending user request (write-safe). +func (bot *Bot) DeletePendingUser(userID int64) { + bot.mu.Lock() + defer bot.mu.Unlock() + delete(bot.WaitingForApproval, userID) +} diff --git a/handlers/join.go b/handlers/join.go new file mode 100644 index 0000000..a67baa4 --- /dev/null +++ b/handlers/join.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "fmt" + "log" + + utils "git.zio.sh/astra/telegram-approval-join/pkg/utils" + api "github.com/OvyFlash/telegram-bot-api" +) + +// HandleJoinRequestResponse records the user's join reason and notifies admins. +func (bot *Bot) HandleJoinRequestResponse(user *ExtendedChatJoinRequest, update *api.Message) { + if user.JoinReason == "" { + user.JoinReason = utils.EscapeHTML(update.Text) + userString := utils.BuildUserString(&user.From) + + keyboard := utils.NewApprovalKeyboard(user.From.ID) + utils.EditMessageWithKeyboard(bot.API, *bot.Config.AdminChatId, user.JoinRequestMessageID, + fmt.Sprintf(AdminJoinRequestMsg, + userString, user.From.ID, user.JoinReason), &keyboard) + + utils.SendMessage(bot.API, update.From.ID, 0, "Thank you! Your request has been sent to the admins for review.") + } else { + utils.SendMessage(bot.API, update.From.ID, 0, "Your request is already pending approval.") + } +} + +// HandleJoinRequest initiates join approval flow by sending entry message and admin notification. +func (bot *Bot) HandleJoinRequest(request *api.ChatJoinRequest) { + // if chat is not in config, ignore + if *bot.Config.TargetChatId != request.Chat.ID { + m := api.NewMessage(*bot.Config.AdminChatId, + fmt.Sprintf("Received join request for chat %s (%d), but it's not in config, ignoring", + request.Chat.Title, request.Chat.ID)) + leaveBtn := api.NewInlineKeyboardButtonData("Leave Chat", fmt.Sprintf("leave_%d", request.Chat.ID)) + m.ReplyMarkup = api.NewInlineKeyboardMarkup([]api.InlineKeyboardButton{leaveBtn}) + m.ParseMode = api.ModeHTML + bot.API.Send(m) + return + } + + utils.SendMessage(bot.API, request.From.ID, 0, bot.Config.EntryMessage) + userString := utils.BuildUserString(&request.From) + + m := api.NewMessage(*bot.Config.AdminChatId, + fmt.Sprintf(AdminJoinRequestMsg, userString, request.From.ID, "(awaiting user response)")) + m.ReplyMarkup = utils.NewApprovalKeyboard(request.From.ID) + m.ParseMode = api.ModeHTML + if topic := *bot.Config.AdminChatTopicId; topic != 0 { + m.MessageThreadID = topic + } + r, err := bot.API.Send(m) + if err != nil { + log.Printf("Failed to send join request to admin chat: %v", err) + return + } + + bot.SetPendingUser(request.From.ID, &ExtendedChatJoinRequest{ + ChatJoinRequest: request, + JoinReason: "", + JoinRequestMessageID: r.MessageID, + }) +} + +// SendFailureMessage edits the admin request message to show a failure status. +func (bot *Bot) SendFailureMessage(user *ExtendedChatJoinRequest, query *api.CallbackQuery, userString string) { + utils.EditMessage(bot.API, *bot.Config.AdminChatId, user.JoinRequestMessageID, + fmt.Sprintf(AdminFailedMsg, userString, user.From.ID, utils.EscapeHTML(user.JoinReason), "User not found in requests.")) + + callback := api.NewCallback(query.ID, "Join request failed.") + bot.API.Request(callback) +} diff --git a/main.go b/main.go index 7d32d8b..e39ff3f 100644 --- a/main.go +++ b/main.go @@ -1,49 +1,31 @@ package main import ( - "fmt" "log" - "strconv" - "strings" - "time" + "git.zio.sh/astra/telegram-approval-join/config" + "git.zio.sh/astra/telegram-approval-join/handlers" api "github.com/OvyFlash/telegram-bot-api" ) -const ( - AdminJoinRequestMsg = "New join #request from %s [%d]\n\nJoin reason: %s" - AdminApprovedMsg = "✅ Join #request approved for %s [%d]\n\nJoin reason: %s\nApproved by: %s\nApproved at: %s" - AdminDeclinedMsg = "❌ Join #request declined for %s [%d]\n\nJoin reason: %s\nDeclined by: %s\nDeclined at: %s\nDeclined reason: %s" - AdminFailedMsg = "⚠️ Join #request failed for %s [%d]\n\nJoin reason: %s\nFailure reason: %s" -) - -type ExtendedChatJoinRequest struct { - *api.ChatJoinRequest - JoinReason string - JoinRequestMessageID int -} - -type Bot struct { - API *api.BotAPI - WaitingForApproval map[int64]*ExtendedChatJoinRequest - Config Config -} - func main() { - b := &Bot{} - b.Config = Config{} + b := &handlers.Bot{} + b.Config = config.Config{} err := b.Config.LoadConfig() if err != nil { log.Fatal(err.Error()) } - bot, err := api.NewBotAPI(b.Config.BotToken) + if b.Config.BotToken == nil { + log.Fatal("Edit config.yaml and fill out bot token") + } + bot, err := api.NewBotAPI(*b.Config.BotToken) if err != nil { panic(err) } b.API = bot - b.WaitingForApproval = make(map[int64]*ExtendedChatJoinRequest) + b.WaitingForApproval = make(map[int64]*handlers.ExtendedChatJoinRequest) log.Printf("Authorized on account %s", bot.Self.UserName) @@ -53,14 +35,14 @@ func main() { for update := range updatesChannel { if update.ChatJoinRequest != nil { - if update.ChatJoinRequest.Chat.ID == b.Config.TargetChatId { - b.handleJoinRequest(update.ChatJoinRequest) + if update.ChatJoinRequest.Chat.ID == *b.Config.TargetChatId { + b.HandleJoinRequest(update.ChatJoinRequest) } continue } if update.CallbackQuery != nil { - b.handleCallbackQuery(update.CallbackQuery) + b.HandleCallbackQuery(update.CallbackQuery) continue } @@ -70,257 +52,12 @@ func main() { if user, ok := b.WaitingForApproval[update.Message.From.ID]; ok { if update.Message.Chat.ID == update.Message.From.ID { - b.handleJoinRequestResponse(user, update.Message) + b.HandleJoinRequestResponse(user, update.Message) } } - if update.Message.Chat.ID == b.Config.AdminChatId && update.Message.ReplyToMessage != nil { - // handle admin declineion reason - repliedMsg := update.Message.ReplyToMessage - if strings.Contains(repliedMsg.Text, "(no reason provided, reply to this to set one, prepend with + to also send to user)") { - // now we need to make sure the one that declined it is the one replying - lines := strings.Split(repliedMsg.Text, "\n") - userString := "" - if update.Message.From.UserName != "" { - userString = "@" + update.Message.From.UserName - } else { - userString = fmt.Sprintf("%s %s", update.Message.From.FirstName, update.Message.From.LastName) - } - if strings.TrimPrefix(lines[3], "Declined by: ") == userString { - reason := EscapeHTML(update.Message.Text) - userID, username, joinReason, declinedBy, declinedAt := GetInfoFromMsg(repliedMsg.Text) - if strings.HasPrefix(update.Message.Text, "+") { - reason = EscapeHTML(update.Message.Text[1:]) - m := api.NewMessage(userID, fmt.Sprintf("Your join request was declined for the following reason:\n\n%s", - reason)) - b.API.Send(m) - } - edit := api.NewEditMessageText(b.Config.AdminChatId, repliedMsg.MessageID, - fmt.Sprintf(AdminDeclinedMsg, - username, userID, joinReason, declinedBy, declinedAt, reason)) - edit.ParseMode = api.ModeHTML - b.API.Send(edit) - } - } + if update.Message.Chat.ID == *b.Config.AdminChatId && update.Message.ReplyToMessage != nil { + b.HandleDeclineReason(&update) } } } - -func (bot *Bot) handleJoinRequestResponse(user *ExtendedChatJoinRequest, update *api.Message) { - if user.JoinReason == "" { - user.JoinReason = EscapeHTML(update.Text) - - userString := "" - if user.From.UserName != "" { - userString = "@" + user.From.UserName - } else { - userString = fmt.Sprintf("%s %s", user.From.FirstName, user.From.LastName) - } - edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID, - fmt.Sprintf(AdminJoinRequestMsg, - userString, user.From.ID, user.JoinReason)) - keyboard := NewApprovalKeyboard(user.From.ID) - edit.ReplyMarkup = &keyboard - edit.ParseMode = api.ModeHTML - bot.API.Send(edit) - - ack := api.NewMessage(update.From.ID, "Thank you! Your request has been sent to the admins for review.") - bot.API.Send(ack) - } else { - m := api.NewMessage(update.From.ID, "Your request is already pending approval.") - bot.API.Send(m) - } -} - -func (bot *Bot) handleJoinRequest(request *api.ChatJoinRequest) { - bot.WaitingForApproval[request.From.ID] = &ExtendedChatJoinRequest{ - ChatJoinRequest: request, - JoinReason: "", - } - m := api.NewMessage(request.From.ID, bot.Config.EntryMessage) - m.ParseMode = api.ModeHTML - bot.API.Send(m) - - userString := "" - if request.From.UserName != "" { - userString = "@" + request.From.UserName - } else { - userString = fmt.Sprintf("%s %s", request.From.FirstName, request.From.LastName) - } - m = api.NewMessage(bot.Config.AdminChatId, - fmt.Sprintf(AdminJoinRequestMsg, userString, request.From.ID, "(awaiting user response)")) - m.ReplyMarkup = NewApprovalKeyboard(request.From.ID) - m.ParseMode = api.ModeHTML - if bot.Config.AdminChatTopicId != 0 { - m.MessageThreadID = bot.Config.AdminChatTopicId - } - r, err := bot.API.Send(m) - if err != nil { - log.Printf("Failed to send join request to admin chat: %v", err) - return - } - bot.WaitingForApproval[request.From.ID].JoinRequestMessageID = r.MessageID -} - -func (bot *Bot) handleCallbackQuery(query *api.CallbackQuery) { - data := strings.Join(strings.Split(query.Data, "_"), " ") - var userId int64 - var action string - _, err := fmt.Sscanf(data, "%s %d", &action, &userId) - if err != nil { - log.Printf("Failed to parse callback data: %v", err) - return - } - - // handle callbacks from admin chat - if query.Message.Chat.ID == bot.Config.AdminChatId { - user, exists := bot.WaitingForApproval[userId] - if !exists { - log.Printf("No pending request for user ID %d", userId) - return - } - - userString := "" - if user.From.UserName != "" { - userString = "@" + user.From.UserName - } else { - userString = fmt.Sprintf("%s %s (no username)", user.From.FirstName, user.From.LastName) - } - - adminUserString := "" - if query.From.UserName != "" { - adminUserString = "@" + query.From.UserName - } else { - adminUserString = fmt.Sprintf("%s %s", query.From.FirstName, query.From.LastName) - } - - switch action { - case "approve": - r := api.ApproveChatJoinRequestConfig{ - ChatConfig: api.ChatConfig{ - ChatID: user.ChatJoinRequest.Chat.ID, - }, - UserID: user.ChatJoinRequest.From.ID, - } - _, e := bot.API.Request(r) - if e != nil { - log.Println(e.Error()) - bot.sendFailureMessage(user, query, userString) - return - } - - edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID, - fmt.Sprintf(AdminApprovedMsg, - userString, user.From.ID, user.JoinReason, adminUserString, time.Now().Format("2006-01-02 15:04:05"))) - edit.ParseMode = api.ModeHTML - bot.API.Send(edit) - - if bot.Config.ApprovalMessage != "" { - m := api.NewMessage(user.From.ID, bot.Config.ApprovalMessage) - m.ParseMode = api.ModeHTML - bot.API.Send(m) - } - - callback := api.NewCallback(query.ID, "Join request approved.") - bot.API.Request(callback) - - case "decline": - r := api.DeclineChatJoinRequest{ - ChatConfig: api.ChatConfig{ - ChatID: user.ChatJoinRequest.Chat.ID, - }, - UserID: user.ChatJoinRequest.From.ID, - } - _, e := bot.API.Request(r) - if e != nil { - log.Println(e.Error()) - bot.sendFailureMessage(user, query, userString) - return - } - - edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID, - fmt.Sprintf(AdminDeclinedMsg, - userString, user.From.ID, user.JoinReason, adminUserString, time.Now().Format("2006-01-02 15:04:05"), - "(no reason provided, reply to this to set one, prepend with + to also send to user)")) - edit.ParseMode = api.ModeHTML - bot.API.Send(edit) - - callback := api.NewCallback(query.ID, "Join request declined.") - bot.API.Request(callback) - } - - if bot.Config.DeleteRequestAfterDecision { - deleteTimer := time.NewTimer(10 * time.Second) - go func() { - <-deleteTimer.C - - del := api.NewDeleteMessage(bot.Config.AdminChatId, user.JoinRequestMessageID) - bot.API.Send(del) - }() - } - - delete(bot.WaitingForApproval, userId) - } -} - -func (bot *Bot) sendFailureMessage(user *ExtendedChatJoinRequest, query *api.CallbackQuery, userString string) { - edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID, - fmt.Sprintf(AdminFailedMsg, userString, user.From.ID, user.JoinReason, "User not found in requests.")) - edit.ParseMode = api.ModeHTML - bot.API.Send(edit) - - callback := api.NewCallback(query.ID, "Join request failed.") - bot.API.Request(callback) -} - -func EscapeMarkdown(s string) string { - toEscape := []string{"*", "_", "`", "[", "]", "(", ")", "\\", "#", "-"} - - replacements := make([]string, 0, len(toEscape)*2) - for _, char := range toEscape { - replacements = append(replacements, char, "\\"+char) - } - - replacer := strings.NewReplacer(replacements...) - return replacer.Replace(s) -} - -func EscapeHTML(s string) string { - toEscape := []string{"&", "<", ">", "\"", "'"} - - replacements := make([]string, 0, len(toEscape)*2) - replacements = append(replacements, "&", "&") - replacements = append(replacements, "<", "<") - replacements = append(replacements, ">", ">") - replacements = append(replacements, "\"", """) - - replacer := strings.NewReplacer(replacements...) - return replacer.Replace(s) -} - -func NewApprovalKeyboard(userID int64) api.InlineKeyboardMarkup { - approveBtn := api.NewInlineKeyboardButtonData("Approve", fmt.Sprintf("approve_%d", userID)) - declineBtn := api.NewInlineKeyboardButtonData("Decline", fmt.Sprintf("decline_%d", userID)) - return api.NewInlineKeyboardMarkup([]api.InlineKeyboardButton{approveBtn, declineBtn}) -} - -func GetInfoFromMsg(msg string) (userId int64, username, joinReason, declinedBy, declinedAt string) { - lines := strings.Split(msg, "\n") - - joinReason = string([]rune(lines[2])[len([]rune("Join reason: ")):]) - declinedBy = string([]rune(lines[3])[len([]rune("Declined by: ")):]) - declinedAt = string([]rune(lines[4])[len([]rune("Declined at: ")):]) - index := LastIndexRuneInRunes([]rune(lines[0]), '[') - userID, _ := strconv.Atoi(string([]rune(lines[0])[index+1 : len([]rune(lines[0]))-1])) - - return int64(userID), username, joinReason, declinedBy, declinedAt -} - -func LastIndexRuneInRunes(runes []rune, r rune) int { - for i := len(runes) - 1; i >= 0; i-- { - if runes[i] == r { - return i - } - } - return -1 -} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..0c646b3 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,166 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" + "unicode/utf16" + + api "github.com/OvyFlash/telegram-bot-api" +) + +func EscapeMarkdown(s string) string { + toEscape := []string{"*", "_", "`", "[", "]", "(", ")", "\\", "#", "-"} + + replacements := make([]string, 0, len(toEscape)*2) + for _, char := range toEscape { + replacements = append(replacements, char, "\\"+char) + } + + replacer := strings.NewReplacer(replacements...) + return replacer.Replace(s) +} + +func EscapeHTML(s string) string { + toEscape := []string{"&", "<", ">", "\"", "'"} + + replacements := make([]string, 0, len(toEscape)*2) + replacements = append(replacements, "&", "&") + replacements = append(replacements, "<", "<") + replacements = append(replacements, ">", ">") + replacements = append(replacements, "\"", """) + + replacer := strings.NewReplacer(replacements...) + return replacer.Replace(s) +} + +func NewApprovalKeyboard(userID int64) api.InlineKeyboardMarkup { + approveBtn := api.NewInlineKeyboardButtonData("Approve", fmt.Sprintf("approve_%d", userID)) + declineBtn := api.NewInlineKeyboardButtonData("Decline", fmt.Sprintf("decline_%d", userID)) + return api.NewInlineKeyboardMarkup([]api.InlineKeyboardButton{approveBtn, declineBtn}) +} + +func GetInfoFromMsg(msg string) (userId int64, username, joinReason, declinedBy, declinedAt string) { + lines := strings.Split(msg, "\n") + + joinReason = string([]rune(lines[2])[len([]rune("Join reason: ")):]) + declinedBy = string([]rune(lines[3])[len([]rune("Declined by: ")):]) + declinedAt = string([]rune(lines[4])[len([]rune("Declined at: ")):]) + index := LastIndexRuneInRunes([]rune(lines[0]), '[') + userID, _ := strconv.Atoi(string([]rune(lines[0])[index+1 : len([]rune(lines[0]))-1])) + + return int64(userID), EscapeHTML(username), EscapeHTML(joinReason), declinedBy, declinedAt +} + +func LastIndexRuneInRunes(runes []rune, r rune) int { + for i := len(runes) - 1; i >= 0; i-- { + if runes[i] == r { + return i + } + } + return -1 +} + +type EntityWithText struct { + Type string `json:"type"` + Offset int `json:"offset"` + Length int `json:"length"` + Text string `json:"text"` +} + +func FilterEntitiesByTypeWithContext(payload *api.Message, entityType string) ([]EntityWithText, error) { + textRunes := utf16.Encode([]rune(payload.Text)) + var filtered []EntityWithText + for _, entity := range payload.Entities { + if entity.Type == entityType { + endOffset := entity.Offset + entity.Length + + entityText := "" + if entity.Offset < len(textRunes) { + entityText = string(utf16.Decode(textRunes[entity.Offset:endOffset])) + } + + filtered = append(filtered, EntityWithText{ + Type: entity.Type, + Offset: entity.Offset, + Length: entity.Length, + Text: entityText, + }) + } + } + + return filtered, nil +} + +func BuildUserString(user *api.User) string { + if user.UserName != "" { + return "@" + user.UserName + } + + var name strings.Builder + name.WriteString("") + if user.FirstName != "" { + fmt.Fprint(&name, EscapeHTML(user.FirstName)) + } + if user.LastName != "" { + fmt.Fprintf(&name, " %s", EscapeHTML(user.LastName)) + } + name.WriteString("") + return name.String() +} + +func SendMessage(botAPI *api.BotAPI, chatID int64, topicID int, text string) (resp api.Message, err error) { + msg := api.NewMessage(chatID, text) + msg.ParseMode = api.ModeHTML + if topicID != 0 { + msg.MessageThreadID = topicID + } + return botAPI.Send(msg) +} + +func EditMessage(botAPI *api.BotAPI, chatID int64, messageID int, text string) (resp api.Message, err error) { + edit := api.NewEditMessageText(chatID, messageID, text) + edit.ParseMode = api.ModeHTML + return botAPI.Send(edit) +} + +func LeaveChatRequest(botAPI *api.BotAPI, chatIDs []int64) error { + var err error + for _, chatID := range chatIDs { + leaveChatConfig := api.LeaveChatConfig{ChatConfig: api.ChatConfig{ChatID: chatID}} + _, err = botAPI.Request(leaveChatConfig) + if err != nil { + return err + } + } + return err +} + +func EditMessageWithKeyboard(botAPI *api.BotAPI, chatID int64, messageID int, text string, keyboard *api.InlineKeyboardMarkup) { + edit := api.NewEditMessageText(chatID, messageID, text) + edit.ParseMode = api.ModeHTML + if keyboard != nil { + edit.ReplyMarkup = keyboard + } + botAPI.Send(edit) +} + +// ParseIntArg parses a string argument as int, returns (value, error message). +// Error message is empty on success. +func ParseIntArg(arg string) (int, string) { + val, err := strconv.Atoi(arg) + if err != nil { + return 0, "Invalid value" + } + return val, "" +} + +// ParseInt64Arg parses a string argument as int64, returns (value, error message). +// Error message is empty on success. +func ParseInt64Arg(arg string) (int64, string) { + val, err := strconv.ParseInt(arg, 10, 64) + if err != nil { + return 0, "Invalid value" + } + return val, "" +}