package main import ( "encoding/json" "fmt" "io" "log" "math" "net/http" "os" "strconv" "strings" "time" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) var adminCommands = map[string]func(BotController, tgbotapi.Update, User){ "/secret": handleSecretCommand, // activate admin mode via /secret `AdminPass` "/panel": handlePanelCommand, // open bot settings "/usermode": handleDefaultMessage, // temporarly disable admin mode to test ui "/deop": handleDeopCommand, // removes your admin rights at all! "/id": handleDefaultMessage, // to check id of chat "/setchannelid": handleDefaultMessage, // just type it in channel which one is supposed to be lined with bot "/broadcast": handleBroadcastCommand, // use /broadcast `msg` to send msg to every known user } var dubaiLocation, _ = time.LoadLocation("Asia/Dubai") var nearestDates = []time.Time{ time.Date(2025, 3, 28, 18, 0, 0, 0, dubaiLocation), time.Date(2025, 4, 1, 18, 0, 0, 0, dubaiLocation), time.Date(2025, 4, 2, 18, 0, 0, 0, dubaiLocation), } var WeekLabels = []string{ "ВС", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", } func main() { var bc = GetBotController() button, callback := getDateButton(nearestDates[0]) log.Printf("Buttons: %s, %s\n", button, callback) log.Printf("Location: %s\n", dubaiLocation.String()) log.Printf("Diff: %s\n", nearestDates[0].Sub(time.Now())) // TODO: REMOVE for _, date := range nearestDates { event := Event{Date: &date} bc.db.Create(&event) } // Run other background tasks go continiousSyncGSheets(bc) go notifyAboutEvents(bc) for update := range bc.updates { go ProcessUpdate(bc, update) } } func continiousSyncGSheets(bc BotController) { for true { err := bc.SyncPaidUsersToSheet() if err != nil { log.Printf("Error sync: %s\n", err) } time.Sleep(60 * time.Second) } } func notifyAboutEvents(bc BotController) { // TODO: migrate to tasks system for true { events, _ := bc.GetAllEvents() for _, event := range events { delta := event.Date.Sub(time.Now()) if int(math.Ceil(delta.Minutes())) == 8*60 { // 8 hours reservations, _ := bc.GetReservationsByEventID(event.ID) for _, reservation := range reservations { uid := reservation.UserID go func() { msg := tgbotapi.NewMessage(uid, bc.GetBotContent("notify_pre_event")) bc.bot.Send(msg) }() } } } time.Sleep(60 * time.Second) } } func ProcessUpdate(bc BotController, update tgbotapi.Update) { if update.Message != nil { var UserID = update.Message.From.ID user := bc.GetUser(UserID) bc.LogMessage(update) log.Printf("Surname: %s\n", update.SentFrom().LastName) bc.UpdateUserInfo(GetUserInfo(update.SentFrom())) // TODO: REMOVE reservation := Reservation{UserID: UserID, EventID: 1, Status: Paid} bc.db.Create(&reservation) text := update.Message.Text if strings.HasPrefix(text, "/") { handleCommand(bc, update, user) } else { handleDefaultMessage(bc, update, user) } } else if update.CallbackQuery != nil { handleCallbackQuery(bc, update) } else if update.ChannelPost != nil { handleChannelPost(bc, update) } } func handleCommand(bc BotController, update tgbotapi.Update, user User) { msg := update.Message log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) log.Printf("[Entities] %s", update.Message.Entities) log.Printf("[COMMAND] %s", update.Message.Command()) command := "/" + msg.Command() // if it is not a command, then it will simply be "/" if user.IsAdmin() { f, exists := adminCommands[command] // f is a function that handles specified command if exists { f(bc, update, user) return } } // commands for non-admins switch command { case "/start": handleStartCommand(bc, update, user) case "/secret": handleSecretCommand(bc, update, user) } } func handleCallbackQuery(bc BotController, update tgbotapi.Update) { user := bc.GetUser(update.CallbackQuery.From.ID) if update.CallbackQuery.Data == "more_info" { msg := tgbotapi.NewMessage(update.FromChat().ID, bc.GetBotContent("more_info_text")) var entities []tgbotapi.MessageEntity meta, _ := bc.GetBotContentMetadata("more_info_text") json.Unmarshal([]byte(meta), &entities) msg.Entities = entities bc.bot.Send(msg) } else if user.IsEffectiveAdmin() { handleAdminCallback(bc, update, user) } if user.IsAdmin() && update.CallbackQuery.Data == "panel" { handlePanelCallback(bc, update, user) } canswer := tgbotapi.NewCallback(update.CallbackQuery.ID, "") bc.bot.Send(canswer) } func handleChannelPost(bc BotController, update tgbotapi.Update) { post := update.ChannelPost if post.Text == "setchannelid" { bc.SetBotContent("channelid", strconv.FormatInt(post.SenderChat.ID, 10), "") var admins []User bc.db.Where("role_bitmask & 1 = ?", 1).Find(&admins) for _, admin := range admins { bc.bot.Send(tgbotapi.NewMessage(admin.ID, "ChannelID is set to "+strconv.FormatInt(post.SenderChat.ID, 10))) delcmd := tgbotapi.NewDeleteMessage(post.SenderChat.ID, post.MessageID) bc.bot.Send(delcmd) } } } // Helper functions for specific commands func handleStartCommand(bc BotController, update tgbotapi.Update, user User) { bc.db.Model(&user).Update("state", "start") rows := [][]tgbotapi.InlineKeyboardButton{} for _, d := range nearestDates { k, v := getDateButton(d) rows = append(rows, tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardButtonData(k, v), ), ) } rows = append(rows, tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData( bc.GetBotContent("more_info"), "more_info", )), ) kbd := tgbotapi.NewInlineKeyboardMarkup(rows...) img, err := bc.GetBotContentVerbose("preview_image") if err != nil || img == "" { msg := tgbotapi.NewMessage(update.Message.Chat.ID, bc.GetBotContent("start")) msg.ReplyMarkup = kbd var entities []tgbotapi.MessageEntity meta, _ := bc.GetBotContentMetadata("start") json.Unmarshal([]byte(meta), &entities) msg.Entities = entities bc.bot.Send(msg) } else { msg := tgbotapi.NewPhoto(update.Message.Chat.ID, tgbotapi.FileID(img)) msg.Caption = bc.GetBotContent("start") msg.ReplyMarkup = kbd var entities []tgbotapi.MessageEntity meta, _ := bc.GetBotContentMetadata("start") json.Unmarshal([]byte(meta), &entities) msg.CaptionEntities = entities bc.bot.Send(msg) } } func handleSecretCommand(bc BotController, update tgbotapi.Update, user User) { if update.Message.CommandArguments() == bc.cfg.AdminPass || user.IsAdmin() { bc.db.Model(&user).Update("state", "start") bc.db.Model(&user).Update("RoleBitmask", user.RoleBitmask|0b11) // set real admin ID (0b1) and effective admin toggle (0b10) msg := tgbotapi.NewMessage(update.Message.Chat.ID, "You are admin now!") bc.bot.Send(msg) } } func handlePanelCommand(bc BotController, update tgbotapi.Update, user User) { handlePanel(bc, user) } func handleUserModeCommand(bc BotController, update tgbotapi.Update, user User) { bc.db.Model(&user).Update("RoleBitmask", user.RoleBitmask&(^uint(0b10))) log.Printf("Set role bitmask (%b) for user: %d", user.RoleBitmask, user.ID) msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Simulating user experience!") bc.bot.Send(msg) } func handleDeopCommand(bc BotController, update tgbotapi.Update, user User) { bc.db.Model(&user).Update("RoleBitmask", user.RoleBitmask&(^uint(0b11))) log.Printf("Set role bitmask (%b) for user: %d", user.RoleBitmask, user.ID) msg := tgbotapi.NewMessage(update.Message.Chat.ID, "DeOPed you!") bc.bot.Send(msg) } func handleBroadcastCommand(bc BotController, update tgbotapi.Update, user User) { if !user.IsAdmin() { return } var users []User bc.db.Find(&users) for _, user := range users { user = user // TODO!!! } } func handleDefaultMessage(bc BotController, update tgbotapi.Update, user User) { if user.State == "leaveticket" { f := update.Message.From ticket := fmt.Sprintf("User: %s %s\nUsername: %s\nText:\n", f.FirstName, f.LastName, f.UserName) ticket += update.Message.Text chatidstr, err := bc.GetBotContentVerbose("supportchatid") if err != nil { var admins []User bc.db.Where("role_bitmask & 1 = ?", 1).Find(&admins) for _, admin := range admins { msg := tgbotapi.NewMessage(admin.ID, "Support ChatID is not set!!!") msg.Entities = []tgbotapi.MessageEntity{tgbotapi.MessageEntity{ Type: "code", Offset: 1, Length: 2, }} bc.bot.Send(msg) } } chatid, _ := strconv.ParseInt(chatidstr, 10, 64) _, err = bc.bot.Send(tgbotapi.NewMessage(chatid, ticket)) if err != nil { msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Something went wrong, try again...") bc.bot.Send(msg) return } bc.db.Model(&user).Update("state", "start") msg := tgbotapi.NewMessage(update.Message.Chat.ID, bc.GetBotContent("sended_notify")) bc.bot.Send(msg) } else if user.IsEffectiveAdmin() { if user.State != "start" { if strings.HasPrefix(user.State, "imgset:") { Literal := strings.Split(user.State, ":")[1] if update.Message.Text == "unset" { var l BotContent bc.db.First(&l, "Literal", Literal) bc.SetBotContent(Literal, "", "") } maxsize := 0 fileid := "" for _, p := range update.Message.Photo { if p.FileSize > maxsize { fileid = p.FileID maxsize = p.FileSize } } bc.SetBotContent(Literal, fileid, "") bc.db.Model(&user).Update("state", "start") msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Successfully set new image!") bc.bot.Send(msg) } else if strings.HasPrefix(user.State, "stringset:") { Literal := strings.Split(user.State, ":")[1] b, _ := json.Marshal(update.Message.Entities) strEntities := string(b) bc.SetBotContent(Literal, update.Message.Text, strEntities) bc.db.Model(&user).Update("state", "start") msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Successfully set new text!") bc.bot.Send(msg) } } } } func handleLeaveTicketButton(bc BotController, update tgbotapi.Update, user User) { chatidstr, err := bc.GetBotContentVerbose("channelid") if err != nil { var admins []User bc.db.Where("role_bitmask & 1 = ?", 1).Find(&admins) for _, admin := range admins { bc.bot.Send(tgbotapi.NewMessage(admin.ID, "ChannelID is not set!!!")) } } chatid, _ := strconv.ParseInt(chatidstr, 10, 64) member, err := bc.bot.GetChatMember(tgbotapi.GetChatMemberConfig{ ChatConfigWithUser: tgbotapi.ChatConfigWithUser{ UserID: update.CallbackQuery.From.ID, SuperGroupUsername: chatidstr, ChatID: chatid, }, }) if err != nil { if strings.Contains(err.Error(), "chat not found") { bc.bot.Send(tgbotapi.NewMessage(user.ID, "No channel ID is set!!!")) } } log.Printf("M: %s, E: %s", member, err) s := member.Status if s == "member" || s == "creator" || s == "admin" { bc.db.Model(&user).Update("state", "leaveticket") bc.bot.Send(tgbotapi.NewMessage(user.ID, bc.GetBotContent("leaveticket_message"))) } else { link, err := bc.GetBotContentVerbose("channel_link") msg := tgbotapi.NewMessage(user.ID, bc.GetBotContent("subscribe_message")) if err == nil { kbd := tgbotapi.NewInlineKeyboardMarkup( tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonURL("Канал", link)), ) msg.ReplyMarkup = kbd } if err != nil { log.Printf("NO LINK!!!") var admins []User bc.db.Where("role_bitmask & 1 = ?", 1).Find(&admins) for _, admin := range admins { msg := tgbotapi.NewMessage(admin.ID, "Channel link is not set!!!") msg.Entities = []tgbotapi.MessageEntity{tgbotapi.MessageEntity{ Type: "code", Offset: 1, Length: 2, }} bc.bot.Send(msg) } } bc.bot.Send(msg) } } func handleAdminCallback(bc BotController, update tgbotapi.Update, user User) { if strings.HasPrefix(update.CallbackQuery.Data, "update:") { Label := strings.Split(update.CallbackQuery.Data, ":")[1] if Label == "preview_image" { bc.db.Model(&user).Update("state", "imgset:"+Label) } else { bc.db.Model(&user).Update("state", "stringset:"+Label) } bc.bot.Send(tgbotapi.NewMessage(user.ID, "Send me asset (text or picture (NOT as file)).\nSay `unset` to delete image.\nSay /start to cancel action")) } } func handlePanelCallback(bc BotController, update tgbotapi.Update, user User) { handlePanel(bc, user) } func DownloadFile(filepath string, url string) error { // Get the data resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // Create the file out, err := os.Create(filepath) if err != nil { return err } defer out.Close() // Write the body to file _, err = io.Copy(out, resp.Body) return err } func notifyAdminAboutError(bc BotController, errorMessage string) { // admins := getAdmins(bc) // for _, admin := range admins { // bc.bot.Send(tgbotapi.NewMessage(admin.ID, "ChannelID is set to "+strconv.FormatInt(post.SenderChat.ID, 10))) // delcmd := tgbotapi.NewDeleteMessage(post.SenderChat.ID, post.MessageID) // bc.bot.Send(delcmd) // } // Check if AdminID is set in the config adminID := *bc.cfg.AdminID if adminID == 0 { log.Println("AdminID is not set in the configuration.") } msg := tgbotapi.NewMessage( adminID, fmt.Sprintf("Error occurred: %s", errorMessage), ) bc.bot.Send(msg) } func getAdmins(bc BotController) []User { var admins []User bc.db.Where("role_bitmask & 1 = ?", 1).Find(&admins) return admins } func getDateButton(date time.Time) (string, string) { // Format the date as needed, e.g., "2006-01-02" wday := WeekLabels[int(date.Local().Weekday())] formattedDate := strings.Join([]string{ date.Format("02.01.2006"), "(" + wday + ")", "в", date.Format("15:04"), }, " ") // Create a token similar to what GetBotContent accepts token := fmt.Sprintf("date_%s", date.Format("200601021504")) // Example token format return strings.Join([]string{"Пойду", formattedDate}, " "), token } func GetUserInfo(user *tgbotapi.User) UserInfo { return UserInfo{ ID: user.ID, Username: user.UserName, FirstName: user.FirstName, LastName: user.LastName, } }