all repos — escarbot @ 58e84a701c04549efc209f66110fe250c47aa909

Earthbound Café's custom Telegram bot, with lots of cool utilities built-in.

minimal working version
Marco Andronaco andronacomarco@gmail.com
Sun, 31 Dec 2023 12:04:13 +0100
commit

58e84a701c04549efc209f66110fe250c47aa909

A .env.example

@@ -0,0 +1,2 @@

+BOT_TOKEN=1234567890:abcdefghijklmnopqrstuvwxyzABCDEFGHI +API_KEY=itsme
A .gitignore

@@ -0,0 +1,2 @@

+.env +__debug_bin*
A .vscode/launch.json

@@ -0,0 +1,15 @@

+{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}" + } + ] +}
A LICENSE

@@ -0,0 +1,21 @@

+MIT License + +Copyright (c) 2023 Marco Andronaco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
A escarbot.go

@@ -0,0 +1,47 @@

+package main + +import ( + "log" + "os" + + "github.com/BiRabittoh/escarbot/telegram" + "github.com/BiRabittoh/escarbot/webui" + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("No .env file provided.") + } + + botToken := os.Getenv("BOT_TOKEN") + if botToken == "" { + log.Fatal("Please set up your BOT_TOKEN in .env!") + } + + channelId := os.Getenv("CHANNEL_ID") + if channelId == "" { + log.Fatal("Please set up your CHANNEL_ID in .env!") + } + + groupId := os.Getenv("GROUP_ID") + if groupId == "" { + log.Fatal("Please set up your GROUP_ID in .env!") + } + + adminId := os.Getenv("ADMIN_ID") + if adminId == "" { + log.Fatal("Please set up your ADMIN_ID in .env!") + } + + port := os.Getenv("PORT") + if port == "" { + log.Println("PORT not set in .env! Defaulting to 1111.") + port = "1111" + } + + bot := telegram.NewBot(botToken, channelId, groupId, adminId) + ui := webui.NewWebUI(port, bot) + ui.Poll() +}
A go.mod

@@ -0,0 +1,7 @@

+module github.com/BiRabittoh/escarbot + +go 1.21.5 + +require github.com/joho/godotenv v1.5.1 + +require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
A go.sum

@@ -0,0 +1,4 @@

+github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
A telegram/forward.go

@@ -0,0 +1,33 @@

+package telegram + +import ( + "log" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func channelPostHandler(escarbot *EscarBot, message *tgbotapi.Message) { + chatId := message.Chat.ID + if chatId != escarbot.ChannelID { + log.Println("Ignoring message since it did not come from the correct chat_id.") + return + } + + msg := tgbotapi.NewForward(escarbot.GroupID, chatId, message.MessageID) + msg.ReplyToMessageID = message.MessageID + _, err := escarbot.Bot.Send(msg) + if err != nil { + log.Println("Error forwarding message to group:", err) + } +} + +func forwardToAdmin(escarbot *EscarBot, message *tgbotapi.Message) { + if !message.Chat.IsPrivate() { + return + } + msg := tgbotapi.NewForward(escarbot.AdminID, message.Chat.ID, message.MessageID) + _, err := escarbot.Bot.Send(msg) + if err != nil { + log.Println("Error forwarding message to admin:", err) + } +}
A telegram/inline.go

@@ -0,0 +1,44 @@

+package telegram + +import ( + "log" + "strconv" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var emptyKeyboard tgbotapi.InlineKeyboardMarkup +var inlineKeyboardFeedback = tgbotapi.NewInlineKeyboardMarkup( + tgbotapi.NewInlineKeyboardRow( + tgbotapi.NewInlineKeyboardButtonData("✅", "1"), + tgbotapi.NewInlineKeyboardButtonData("❌", "0"), + ), +) + +func callbackQueryHandler(bot *tgbotapi.BotAPI, query *tgbotapi.CallbackQuery) { + res, err := strconv.ParseInt(query.Data, 10, 64) + if err != nil { + log.Println("Could not parse int:", err) + return + } + + var callbackResponse tgbotapi.CallbackConfig + var action tgbotapi.Chattable + + if res == 0 { + callbackResponse = tgbotapi.NewCallback(query.ID, "Ci ho provato...") + action = tgbotapi.NewDeleteMessage(query.Message.Chat.ID, query.Message.MessageID) + } else { + callbackResponse = tgbotapi.NewCallback(query.ID, "Bene!") + action = tgbotapi.NewEditMessageReplyMarkup(query.Message.Chat.ID, query.Message.MessageID, emptyKeyboard) + } + + if _, err := bot.Request(callbackResponse); err != nil { + panic(err) + } + _, err = bot.Request(action) + if err != nil { + log.Fatal(err) + } + +}
A telegram/replace.go

@@ -0,0 +1,100 @@

+package telegram + +import ( + "fmt" + "regexp" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type Replacer struct { + Regex *regexp.Regexp + Format string +} + +var parseMode = "markdown" +var linkMessage = "[🔗](%s) Da %s." +var regexFlags = "(?i)(?m)" +var replacers = []Replacer{ + { + Regex: regexp.MustCompile(regexFlags + `(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:(?:youtube(?:-nocookie)?\.com|youtu.be))(?:\/(?:[\w\-]+\?v=|embed\/|live\/|v\/|shorts\/)?)([\w\-]+)(\S+)?`), + Format: "https://y.outube.duckdns.org/%s", + }, + { + Regex: regexp.MustCompile(regexFlags + `(?:https?:\/\/)(?:www\.)?twitter\.com\/(?:#!\/)?(.*)\/status(?:es)?\/([^\/\?\s]+)`), + Format: "https://fxtwitter.com/%s/status/%s", + }, + { + Regex: regexp.MustCompile(regexFlags + `(?:https?:\/\/)?(?:www\.)?x\.com\/(?:#!\/)?(.*)\/status(?:es)?\/([^\/\?\s]+)`), + Format: "https://fixupx.com/%s/status/%s", + }, + { + Regex: regexp.MustCompile(regexFlags + `(?:https?:\/\/)?(?:www\.)?instagram\.com\/((?:reel)|p)\/([A-Za-z0-9_]{11})[\/\?\w=&]*`), + Format: "https://ddinstagram.com/%s/%s", + }, + { + Regex: regexp.MustCompile(regexFlags + `(?:https?:\/\/)?(?:(?:www)|(?:vm))?\.?tiktok\.com\/@([\w\d_.]+)\/(?:video)\/(\d+)`), + Format: "https://www.vxtiktok.com/@%s/video/%s", + }, + { + Regex: regexp.MustCompile(regexFlags + `(?:https?:\/\/)?(?:(?:www)|(?:vm))?\.?tiktok\.com\/([\w]+)\/?`), + Format: "https://vm.vxtiktok.com/%s/", + }, +} + +func parseText(message string) []string { + links := []string{} + + for _, replacer := range replacers { + foundMatches := replacer.Regex.FindStringSubmatch(message) + + if len(foundMatches) == 0 { + continue + } + captureGroups := foundMatches[1:] + + var formatArgs []interface{} + for _, match := range captureGroups { + if match != "" { + formatArgs = append(formatArgs, match) + } + } + + formatted := fmt.Sprintf(replacer.Format, formatArgs...) + links = append(links, formatted) + } + return links +} + +func getUserMention(user tgbotapi.User) string { + return fmt.Sprintf("[@%s](tg://user?id=%d)", user.UserName, user.ID) +} + +func handleLinks(bot *tgbotapi.BotAPI, message *tgbotapi.Message) { + links := []string{} + + if message.Text != "" { + textLinks := parseText(message.Text) + links = append(links, textLinks...) + } + + if message.Caption != "" { + captionLinks := parseText(message.Caption) + links = append(links, captionLinks...) + } + + if len(links) == 0 { + return + } + + user := getUserMention(*message.From) + + for _, link := range links { + text := fmt.Sprintf(linkMessage, link, user) + msg := tgbotapi.NewMessage(message.Chat.ID, text) + msg.ParseMode = parseMode + msg.ReplyMarkup = inlineKeyboardFeedback + bot.Send(msg) + } + +}
A telegram/telegram.go

@@ -0,0 +1,88 @@

+package telegram + +import ( + "log" + "strconv" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type EscarBot struct { + Bot *tgbotapi.BotAPI + Power bool + LinkDetection bool + ChannelForward bool + AdminForward bool + ChannelID int64 + GroupID int64 + AdminID int64 +} + +func NewBot(botToken string, channelId string, groupId string, adminId string) *EscarBot { + bot, err := tgbotapi.NewBotAPI(botToken) + if err != nil { + log.Panic(err) + } + + //bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + channelIdInt, err := strconv.ParseInt(channelId, 10, 64) + if err != nil { + log.Fatal("Error while converting CHANNEL_ID to int64:", err) + } + + groupIdInt, err := strconv.ParseInt(groupId, 10, 64) + if err != nil { + log.Fatal("Error while converting GROUP_ID to int64:", err) + } + + adminIdInt, err := strconv.ParseInt(adminId, 10, 64) + if err != nil { + log.Fatal("Error while converting ADMIN_ID to int64:", err) + } + + emptyKeyboard = tgbotapi.NewInlineKeyboardMarkup() + emptyKeyboard.InlineKeyboard = [][]tgbotapi.InlineKeyboardButton{} + + return &EscarBot{ + Bot: bot, + Power: true, + LinkDetection: true, + ChannelForward: true, + AdminForward: true, + ChannelID: channelIdInt, + GroupID: groupIdInt, + AdminID: adminIdInt, + } +} + +func BotPoll(escarbot *EscarBot) { + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + bot := escarbot.Bot + updates := bot.GetUpdatesChan(u) + + for update := range updates { + msg := update.Message + if msg != nil { // If we got a message + if escarbot.LinkDetection { + handleLinks(bot, msg) + } + if escarbot.AdminForward { + forwardToAdmin(escarbot, msg) + } + } + if update.ChannelPost != nil { // If we got a channel post + if escarbot.ChannelForward { + channelPostHandler(escarbot, update.ChannelPost) + } + } + query := update.CallbackQuery + if query != nil { // If we got a callback query + callbackQueryHandler(bot, query) + } + } +}
A webui/index.html

@@ -0,0 +1,124 @@

+<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Bot Telegram Control Panel</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/milligram@1.4.1/dist/milligram.min.css"> + <style> + label { + display: inline-block; + } + body { + background-color: #1f1f1f; + color: #fff; + } + button:hover { + background-color: #8842b3; + } + input, textarea { + color: #fff; + resize: vertical; + } + </style> +</head> +<body> + <h1>EscarBot</h1> + + <form action="/setLinks" id="linkForm"> + <label> + <input type="checkbox" id="linkToggle" name="toggle"{{ if .LinkDetection }} checked{{ end }}> + Link detection in group messages + </label> + </form> + + <form action="/setChannelForward" id="channelForwardForm"> + <label> + <input type="checkbox" id="channelForwardToggle" name="toggle"{{ if .ChannelForward }} checked{{ end }}> + Message forwarding (channel) + </label> + </form> + + <form action="/setAdminForward" id="adminForwardForm"> + <label> + <input type="checkbox" id="adminForwardToggle" name="toggle"{{ if .AdminForward }} checked{{ end }}> + Message forwarding (admin) + </label> + </form> + + <form action="/setChannel" id="channelForm"> + <label> + Channel ID: + <input type="text" id="channelId" name="id" value="{{ .ChannelID }}" required> + </label> + <button type="submit">Change</button> + </form> + + <form action="/setGroup" id="groupForm"> + <label> + Group ID: + <input type="text" id="groupId" name="id" value="{{ .GroupID }}" required> + </label> + <button type="submit">Change</button> + </form> + + <form action="/setAdmin" id="adminForm"> + <label> + Admin ID: + <input type="text" id="adminId" name="id" value="{{ .AdminID }}" required> + </label> + <button type="submit">Change</button> + </form> + + <form id="messageForm"> + <label for="customMessage">Custom Message:</label> + <textarea id="customMessage" name="customMessage" rows="4" required></textarea> + <label> + Recipient: + <input type="text" id="recipientId" name="recipientId" value="{{ .GroupID }}" required> + </label> + <button id="customMessageButton">Send</button> + </form> + + <script> + const sendMessageUrl = "https://api.telegram.org/bot{{ .Bot.Token }}/sendMessage" + const messageParams = new URLSearchParams({ + "text": "", + "chat_id": "", + "reply_to_message_id": "", + "parse_mode": "markdown", + "disable_web_page_preview": "false", + "disable_notification": "false", + }) + const customMessageBox = document.getElementById("customMessage"); + function handleToggle(toggleId, formId) { + document.getElementById(toggleId).addEventListener('change', () => { + document.getElementById(formId).submit(); + }); + } + + handleToggle('linkToggle', 'linkForm'); + handleToggle('channelForwardToggle', 'channelForwardForm'); + handleToggle('adminForwardToggle', 'adminForwardForm'); + + document.getElementById("customMessageButton").addEventListener("click", (event) => { + event.preventDefault() + const value = customMessageBox.value; + const recipient = document.getElementById("recipientId").value; + + messageParams.set("text", value); + messageParams.set("chat_id", recipient); + + const request_url = sendMessageUrl + "?" + messageParams; + console.log(request_url); + fetch(request_url).then((response) => { + response.json().then((data) => { + console.log(data); + if (data.ok) customMessageBox.value = ""; + else alert(data.description); + }) + }) + }) + </script> +</body> +</html>
A webui/webui.go

@@ -0,0 +1,128 @@

+package webui + +import ( + "bytes" + "log" + "net/http" + "path" + "strconv" + "text/template" + + "github.com/BiRabittoh/escarbot/telegram" +) + +type WebUI struct { + Server *http.ServeMux + EscarBot *telegram.EscarBot + + port string +} + +var indexTemplate = template.Must(template.ParseFiles(path.Join("webui", "index.html"))) + +const toggleFormName = "toggle" + +func indexHandler(bot *telegram.EscarBot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + buf := &bytes.Buffer{} + err := indexTemplate.Execute(buf, bot) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + buf.WriteTo(w) + } +} + +func toggleBotProperty(w http.ResponseWriter, r *http.Request, bot *telegram.EscarBot) bool { + r.ParseForm() + res := r.Form.Get("toggle") + http.Redirect(w, r, "/", http.StatusFound) + return res == "on" +} + +func getChatID(w http.ResponseWriter, r *http.Request, bot *telegram.EscarBot) (int64, error) { + r.ParseForm() + res := r.Form.Get("id") + http.Redirect(w, r, "/", http.StatusFound) + return strconv.ParseInt(res, 10, 64) +} + +func linksHandler(bot *telegram.EscarBot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + bot.LinkDetection = toggleBotProperty(w, r, bot) + } +} + +func channelForwardHandler(bot *telegram.EscarBot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + bot.ChannelForward = toggleBotProperty(w, r, bot) + } +} + +func adminForwardHandler(bot *telegram.EscarBot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + bot.AdminForward = toggleBotProperty(w, r, bot) + } +} + +func channelHandler(bot *telegram.EscarBot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + res, err := getChatID(w, r, bot) + if err != nil { + log.Println(err) + return + } + bot.ChannelID = res + } +} + +func groupHandler(bot *telegram.EscarBot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + res, err := getChatID(w, r, bot) + if err != nil { + log.Println(err) + return + } + bot.GroupID = res + } +} + +func adminHandler(bot *telegram.EscarBot) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + res, err := getChatID(w, r, bot) + if err != nil { + log.Println(err) + return + } + bot.AdminID = res + } +} + +func NewWebUI(port string, bot *telegram.EscarBot) WebUI { + + go telegram.BotPoll(bot) + + r := http.NewServeMux() + r.HandleFunc("/", indexHandler(bot)) + r.HandleFunc("/setLinks", linksHandler(bot)) + r.HandleFunc("/setChannelForward", channelForwardHandler(bot)) + r.HandleFunc("/setAdminForward", adminForwardHandler(bot)) + r.HandleFunc("/setChannel", channelHandler(bot)) + r.HandleFunc("/setGroup", groupHandler(bot)) + r.HandleFunc("/setAdmin", adminHandler(bot)) + + return WebUI{ + Server: r, + EscarBot: bot, + port: port, + } +} + +func (webui *WebUI) Poll() { + log.Println("Serving on port", webui.port) + err := http.ListenAndServe(":"+webui.port, webui.Server) + if err != nil { + log.Fatal(err) + } +}