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, ""
+}