commit 6f70daebca4f7bb64fef8a30ee1dadfe65d2e13a Author: Astra Date: Wed Jan 28 15:56:14 2026 +0000 initial commit diff --git a/config.go b/config.go new file mode 100644 index 0000000..85f46ae --- /dev/null +++ b/config.go @@ -0,0 +1,49 @@ +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"` +} + +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.", + } + + encoder := yaml.NewEncoder(f) + err = encoder.Encode(defaultConfig) + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ccc0e15 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.zio.sh/astra/telegram-approval-join + +go 1.25.3 + +require ( + github.com/OvyFlash/telegram-bot-api v0.0.0-20251112155921-e82db5fd534b + go.yaml.in/yaml/v3 v3.0.4 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e0ddb19 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/OvyFlash/telegram-bot-api v0.0.0-20251112155921-e82db5fd534b h1:vC+cZNbleRsR1busnocKwnZ3Hm9Bp37QeWH81Dz91g8= +github.com/OvyFlash/telegram-bot-api v0.0.0-20251112155921-e82db5fd534b/go.mod h1:2nRUdsKyWhvezqW/rBGWEQdcTQeTtnbSNd2dgx76WYA= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..317a499 --- /dev/null +++ b/main.go @@ -0,0 +1,237 @@ +package main + +import ( + "fmt" + "log" + "strings" + "time" + + api "github.com/OvyFlash/telegram-bot-api" +) + +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{} + err := b.Config.LoadConfig() + if err != nil { + log.Fatal(err.Error()) + } + + bot, err := api.NewBotAPI(b.Config.BotToken) + if err != nil { + panic(err) + } + + b.API = bot + b.WaitingForApproval = make(map[int64]*ExtendedChatJoinRequest) + + log.Printf("Authorized on account %s", bot.Self.UserName) + + updateConfig := api.NewUpdate(0) + updateConfig.Timeout = 60 + updatesChannel := b.API.GetUpdatesChan(updateConfig) + + var updatesChannelMock chan api.Update = make(chan api.Update, 1) + mockUpdates := []api.Update{ + { + ChatJoinRequest: &api.ChatJoinRequest{ + Chat: api.Chat{ + ID: -1001895847484, + Type: "supergroup", + Title: "Example Group", + }, + From: api.User{ + ID: 55258520, + FirstName: "Astra", + LastName: "Doe", + UserName: "asstra", + }, + Date: int(time.Now().Unix()), + UserChatID: 55258520, + }, + }, + { + Message: &api.Message{ + MessageID: 1, + From: &api.User{ + ID: 55258520, + FirstName: "Astra", + LastName: "Doe", + UserName: "asstra", + }, + Chat: api.Chat{ + ID: 55258520, + Type: "private", + }, + Date: int(time.Now().Unix()), + Text: "I would like to join because *I* test love this group!", + }, + }, + } + + go func() { + for _, update := range mockUpdates { + updatesChannelMock <- update + time.Sleep(2 * time.Second) // delay between updates + } + close(updatesChannelMock) + }() + + for update := range updatesChannel { //Mock { + if update.ChatJoinRequest != nil { + b.handleJoinRequest(update.ChatJoinRequest) + continue + } + + if update.CallbackQuery != nil { + b.handleCallbackQuery(update.CallbackQuery) + continue + } + + if update.Message == nil { + continue + } + + if user, ok := b.WaitingForApproval[update.Message.From.ID]; ok { + if update.Message.Chat.ID == update.Message.From.ID { + b.handleJoinRequestResponse(user, update.Message) + } + } + } +} + +func (bot *Bot) handleJoinRequestResponse(user *ExtendedChatJoinRequest, update *api.Message) { + if user.JoinReason == "" { + user.JoinReason = escapeMarkdown(update.Text) + + edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID, + fmt.Sprintf("New join request from _%s_\n\nJoin reason: %s", + user.From.String(), user.JoinReason)) + approveButton := api.NewInlineKeyboardButtonData("✅ Approve", fmt.Sprintf("approve_%d", user.From.ID)) + rejectButton := api.NewInlineKeyboardButtonData("❌ Reject", fmt.Sprintf("reject_%d", user.From.ID)) + keyboard := api.NewInlineKeyboardMarkup( + []api.InlineKeyboardButton{approveButton, rejectButton}, + ) + edit.ReplyMarkup = &keyboard + edit.ParseMode = api.ModeMarkdown + 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 { + if user.Date+(60*60*5) < int(time.Now().Unix()) { + + } else { + m := api.NewMessage(update.From.ID, "Your request is already pending approval.") + bot.API.Send(m) + } + } +} + +func (bot *Bot) handleJoinRequest(request *api.ChatJoinRequest) { + if bot.Config.TargetChatId == request.Chat.ID { + bot.WaitingForApproval[request.From.ID] = &ExtendedChatJoinRequest{ + ChatJoinRequest: request, + JoinReason: "", + } + m := api.NewMessage(request.From.ID, bot.Config.EntryMessage) + m.ParseMode = api.ModeMarkdown + bot.API.Send(m) + + m = api.NewMessage(bot.Config.AdminChatId, + fmt.Sprintf("New join request from _%s_\n\nJoin reason: (no reason provided yet)", + request.From.String())) + approveButton := api.NewInlineKeyboardButtonData("✅ Approve", fmt.Sprintf("approve_%d", request.From.ID)) + rejectButton := api.NewInlineKeyboardButtonData("❌ Reject", fmt.Sprintf("reject_%d", request.From.ID)) + keyboard := api.NewInlineKeyboardMarkup( + []api.InlineKeyboardButton{approveButton, rejectButton}, + ) + m.ReplyMarkup = keyboard + m.ParseMode = api.ModeMarkdown + if bot.Config.AdminChatTopicId != 0 { + m.MessageThreadID = bot.Config.AdminChatTopicId + } + r, _ := bot.API.Send(m) + 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 + } + + switch action { + case "approve": + r := api.ApproveChatJoinRequestConfig{ + ChatConfig: api.ChatConfig{ + ChatID: user.ChatJoinRequest.Chat.ID, + }, + UserID: user.ChatJoinRequest.From.ID, + } + bot.API.Send(r) + edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID, + fmt.Sprintf("✅ Join #request approved for _%s_\n\nJoin reason: %s\nApproved by: %s\nApproved at: %s", + user.From.String(), user.JoinReason, query.From.String(), time.Now().Format("2006-01-02 15:04:05"))) + edit.ParseMode = api.ModeMarkdown + bot.API.Send(edit) + + m := api.NewMessage(user.From.ID, bot.Config.ApprovalMessage) + m.ParseMode = api.ModeMarkdown + bot.API.Send(m) + + case "reject": + r := api.DeclineChatJoinRequest{ + ChatConfig: api.ChatConfig{ + ChatID: user.ChatJoinRequest.Chat.ID, + }, + UserID: user.ChatJoinRequest.From.ID, + } + bot.API.Send(r) + edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID, + fmt.Sprintf("❌ Join #request rejected for _%s_\n\nJoin reason: %s\nRejected by: %s\nRejected at: %s", + user.From.String(), user.JoinReason, query.From.String(), time.Now().Format("2006-01-02 15:04:05"))) + edit.ParseMode = api.ModeMarkdown + bot.API.Send(edit) + } + + delete(bot.WaitingForApproval, userId) + } +} + +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) +}