telegram-join-approval-nuzzles/main.go

284 lines
8.9 KiB
Go

package main
import (
"fmt"
"log"
"strconv"
"strings"
"time"
api "github.com/OvyFlash/telegram-bot-api"
)
const (
AdminJoinRequestMsg = "New join #request from <i>%s</i> [<code>%d</code>]\n\nJoin reason: %s"
AdminApprovedMsg = "✅ Join #request approved for <i>%s</i> [<code>%d</code>]\n\nJoin reason: %s\nApproved by: %s\nApproved at: %s"
AdminDeclinedMsg = "❌ Join #request declined for <i>%s</i> [<code>%d</code>]\n\nJoin reason: %s\nDeclined by: %s\nDeclined at: %s\nDeclined 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{}
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)
for update := range updatesChannel {
if update.ChatJoinRequest != nil {
if update.ChatJoinRequest.Chat.ID == b.Config.TargetChatId {
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)
}
}
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 send 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")
if strings.TrimPrefix(lines[3], "Declined by: ") == update.Message.From.String() {
reason := escapeHTML(update.Message.Text)
if after, ok := strings.CutPrefix(update.Message.Text, "+"); ok {
reason = escapeHTML(after)
}
userID, username, joinReason, declinedBy, declinedAt := GetInfoFromMsg(repliedMsg.Text)
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)
m := api.NewMessage(userID, fmt.Sprintf("Your join request was declined for the following reason:\n\n%s",
update.Message.Text))
b.API.Send(m)
}
}
}
}
}
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 (no username)", 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 (no username)", 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, _ := 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)
userString := ""
if user.From.UserName != "" {
userString = "@" + user.From.UserName
} else {
userString = fmt.Sprintf("%s %s (no username)", user.From.FirstName, user.From.LastName)
}
edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID,
fmt.Sprintf(AdminApprovedMsg,
userString, user.From.ID, user.JoinReason, query.From.String(), 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,
}
bot.API.Send(r)
userString := ""
if user.From.UserName != "" {
userString = "@" + user.From.UserName
} else {
userString = fmt.Sprintf("%s %s (no username)", user.From.FirstName, user.From.LastName)
}
edit := api.NewEditMessageText(bot.Config.AdminChatId, user.JoinRequestMessageID,
fmt.Sprintf(AdminDeclinedMsg,
userString, user.From.ID, user.JoinReason, query.From.String(), time.Now().Format("2006-01-02 15:04:05"), "(no reason provided, reply to this to send 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 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, "&", "&amp;")
replacements = append(replacements, "<", "&lt;")
replacements = append(replacements, ">", "&gt;")
replacements = append(replacements, "\"", "&quot;")
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) {
start := strings.Index(msg, "[")
end := strings.Index(msg, "]")
userID, _ := strconv.Atoi(msg[start+1 : end])
username = msg[31 : start-1]
lines := strings.Split(msg, "\n")
joinReason = strings.TrimPrefix(lines[2], "Join reason: ")
declinedBy = strings.TrimPrefix(lines[3], "Declined by: ")
declinedAt = strings.TrimPrefix(lines[4], "Declined at: ")
return int64(userID), username, joinReason, declinedBy, declinedAt
}