This commit is contained in:
Akulij 2025-03-29 19:19:47 +08:00
parent 8b6317b31a
commit ed99f4a16f
5 changed files with 144 additions and 138 deletions

View File

@ -27,10 +27,10 @@ func (bc BotController) GetUserByID(UserID int64) (User, error) {
type UserInfo struct { type UserInfo struct {
gorm.Model gorm.Model
ID int64 ID int64
Username string Username string
FirstName string FirstName string
LastName string LastName string
} }
func (u User) IsAdmin() bool { func (u User) IsAdmin() bool {
@ -45,7 +45,7 @@ type BotContent struct {
gorm.Model gorm.Model
Literal string Literal string
Content string Content string
Metadata string Metadata string
} }
func GetDB() (*gorm.DB, error) { func GetDB() (*gorm.DB, error) {
@ -89,7 +89,7 @@ func (bc BotController) GetBotContentMetadata(Literal string) (string, error) {
} }
func (bc BotController) SetBotContent(Literal string, Content string, Metadata string) { func (bc BotController) SetBotContent(Literal string, Content string, Metadata string) {
c := BotContent{Literal: Literal, Content: Content, Metadata: Metadata} c := BotContent{Literal: Literal, Content: Content, Metadata: Metadata}
bc.db.FirstOrCreate(&c, "Literal", Literal) bc.db.FirstOrCreate(&c, "Literal", Literal)
bc.db.Model(&c).Update("Content", Content) bc.db.Model(&c).Update("Content", Content)
bc.db.Model(&c).Update("Metadata", Metadata) bc.db.Model(&c).Update("Metadata", Metadata)
@ -108,7 +108,7 @@ func (bc BotController) GetUser(UserID int64) User {
} }
func (bc BotController) UpdateUserInfo(ui UserInfo) { func (bc BotController) UpdateUserInfo(ui UserInfo) {
bc.db.Save(&ui) bc.db.Save(&ui)
} }
func (bc BotController) GetUserInfo(UserID int64) (UserInfo, error) { func (bc BotController) GetUserInfo(UserID int64) (UserInfo, error) {
@ -116,7 +116,7 @@ func (bc BotController) GetUserInfo(UserID int64) (UserInfo, error) {
bc.db.First(&ui, "ID", UserID) bc.db.First(&ui, "ID", UserID)
if ui == (UserInfo{}) { if ui == (UserInfo{}) {
log.Printf("NO UserInfo FOUND!!!, id: [%d]", UserID) log.Printf("NO UserInfo FOUND!!!, id: [%d]", UserID)
return UserInfo{}, errors.New("NO UserInfo FOUND!!!") return UserInfo{}, errors.New("NO UserInfo FOUND!!!")
} }
return ui, nil return ui, nil
@ -139,22 +139,24 @@ func (bc BotController) LogMessageRaw(UserID int64, Msg string, Time time.Time)
} }
type ReservationStatus int64 type ReservationStatus int64
const ( const (
Booked ReservationStatus = iota Booked ReservationStatus = iota
Paid Paid
) )
var ReservationStatusString = []string{ var ReservationStatusString = []string{
"Забронировано", "Забронировано",
"Оплачено", "Оплачено",
} }
type Reservation struct { type Reservation struct {
gorm.Model gorm.Model
ID int64 `gorm:"primary_key"` ID int64 `gorm:"primary_key"`
UserID int64 `gorm:"uniqueIndex:user_event_uniq"` UserID int64 `gorm:"uniqueIndex:user_event_uniq"`
TimeBooked *time.Time TimeBooked *time.Time
EventID int64 `gorm:"uniqueIndex:user_event_uniq"` EventID int64 `gorm:"uniqueIndex:user_event_uniq"`
Status ReservationStatus Status ReservationStatus
} }
func (bc BotController) GetAllReservations() ([]Reservation, error) { func (bc BotController) GetAllReservations() ([]Reservation, error) {
@ -176,9 +178,9 @@ func (bc BotController) GetReservationsByEventID(EventID int64) ([]Reservation,
} }
type Event struct { type Event struct {
gorm.Model gorm.Model
ID int64 `gorm:"primary_key"` ID int64 `gorm:"primary_key"`
Date *time.Time `gorm:"unique"` Date *time.Time `gorm:"unique"`
} }
func (bc BotController) GetAllEvents() ([]Event, error) { func (bc BotController) GetAllEvents() ([]Event, error) {
@ -200,23 +202,24 @@ func (bc BotController) GetEvent(EventID int64) (Event, error) {
} }
type TaskType int64 type TaskType int64
const ( const (
SyncSheet TaskType = iota SyncSheet TaskType = iota
NotifyAboutEvent NotifyAboutEvent
) )
type Task struct { type Task struct {
gorm.Model gorm.Model
ID int64 `gorm:"primary_key"` ID int64 `gorm:"primary_key"`
Type TaskType Type TaskType
EventID int64 EventID int64
} }
func (bc BotController) CreateSimpleTask(taskType TaskType) error { func (bc BotController) CreateSimpleTask(taskType TaskType) error {
task := Task{ task := Task{
Type: taskType, Type: taskType,
} }
return bc.CreateTask(task) return bc.CreateTask(task)
} }
func (bc BotController) CreateTask(task Task) error { func (bc BotController) CreateTask(task Task) error {

View File

@ -4,35 +4,37 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"time" "time"
"google.golang.org/api/option" "google.golang.org/api/option"
"google.golang.org/api/sheets/v4" "google.golang.org/api/sheets/v4"
) )
func (bc *BotController) SyncPaidUsersToSheet() error { func (bc *BotController) SyncPaidUsersToSheet() error {
reservations, _ := bc.GetAllReservations() reservations, _ := bc.GetAllReservations()
ctx := context.Background() ctx := context.Background()
srv, err := sheets.NewService(ctx, srv, err := sheets.NewService(ctx,
option.WithCredentialsFile("./credentials.json"), option.WithCredentialsFile("./credentials.json"),
option.WithScopes(sheets.SpreadsheetsScope), option.WithScopes(sheets.SpreadsheetsScope),
) )
if err != nil { if err != nil {
return fmt.Errorf("unable to retrieve Sheets client: %v", err) return fmt.Errorf("unable to retrieve Sheets client: %v", err)
} }
var values [][]interface{} var values [][]interface{}
values = append(values, []interface{}{"Телеграм ID", "Имя", "Фамилия", "Никнейм", "Указанное имя", "Дата", "Телефон", "Статус"}) values = append(values, []interface{}{"Телеграм ID", "Имя", "Фамилия", "Никнейм", "Указанное имя", "Дата", "Телефон", "Статус"})
for _, reservation := range reservations { for _, reservation := range reservations {
if reservation.Status != Paid {continue} if reservation.Status != Paid {
continue
}
uid := reservation.UserID uid := reservation.UserID
user, _ := bc.GetUserByID(uid) user, _ := bc.GetUserByID(uid)
ui, _ := bc.GetUserInfo(uid) ui, _ := bc.GetUserInfo(uid)
event, _ := bc.GetEvent(reservation.EventID) event, _ := bc.GetEvent(reservation.EventID)
status := ReservationStatusString[reservation.Status] status := ReservationStatusString[reservation.Status]
values = append(values, []interface{}{user.ID, ui.FirstName, ui.LastName, ui.Username, "TODO", formatDate(event.Date), "", status}) values = append(values, []interface{}{user.ID, ui.FirstName, ui.LastName, ui.Username, "TODO", formatDate(event.Date), "", status})
} }
@ -55,5 +57,5 @@ func (bc *BotController) SyncPaidUsersToSheet() error {
} }
func formatDate(t *time.Time) string { func formatDate(t *time.Time) string {
return t.Format("02.01 15:04") return t.Format("02.01 15:04")
} }

View File

@ -1,16 +1,16 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"time"
"io" "io"
"log" "log"
"math"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"encoding/json" "time"
"math"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
@ -22,45 +22,44 @@ var adminCommands = map[string]func(BotController, tgbotapi.Update, User){
"/deop": handleDeopCommand, // removes your admin rights at all! "/deop": handleDeopCommand, // removes your admin rights at all!
"/id": handleDefaultMessage, // to check id of chat "/id": handleDefaultMessage, // to check id of chat
"/setchannelid": handleDefaultMessage, // just type it in channel which one is supposed to be lined with bot "/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 "/broadcast": handleBroadcastCommand, // use /broadcast `msg` to send msg to every known user
} }
var dubaiLocation, _ = time.LoadLocation("Asia/Dubai") var dubaiLocation, _ = time.LoadLocation("Asia/Dubai")
var nearestDates = []time.Time{ var nearestDates = []time.Time{
time.Date(2025, 3, 28, 18, 0, 0, 0, dubaiLocation), time.Date(2025, 3, 28, 18, 0, 0, 0, dubaiLocation),
time.Date(2025, 4, 1, 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), time.Date(2025, 4, 2, 18, 0, 0, 0, dubaiLocation),
} }
var WeekLabels = []string{ var WeekLabels = []string{
"None", "None",
"ПН", "ПН",
"ВТ", "ВТ",
"СР", "СР",
"ЧТ", "ЧТ",
"ПТ", "ПТ",
"СБ", "СБ",
"ВС", "ВС",
} }
func main() { func main() {
var bc = GetBotController() var bc = GetBotController()
button, callback := getDateButton(nearestDates[0]) button, callback := getDateButton(nearestDates[0])
log.Printf("Buttons: %s, %s\n", button, callback) log.Printf("Buttons: %s, %s\n", button, callback)
log.Printf("Location: %s\n", dubaiLocation.String()) log.Printf("Location: %s\n", dubaiLocation.String())
log.Printf("Diff: %s\n", nearestDates[0].Sub(time.Now())) log.Printf("Diff: %s\n", nearestDates[0].Sub(time.Now()))
// TODO: REMOVE
for _, date := range nearestDates {
event := Event{Date: &date}
bc.db.Create(&event)
}
// TODO: REMOVE // Run other background tasks
for _, date := range nearestDates { go continiousSyncGSheets(bc)
event := Event{Date: &date} go notifyAboutEvents(bc)
bc.db.Create(&event)
}
// Run other background tasks
go continiousSyncGSheets(bc)
go notifyAboutEvents(bc)
for update := range bc.updates { for update := range bc.updates {
go ProcessUpdate(bc, update) go ProcessUpdate(bc, update)
@ -68,35 +67,35 @@ func main() {
} }
func continiousSyncGSheets(bc BotController) { func continiousSyncGSheets(bc BotController) {
for true { for true {
err := bc.SyncPaidUsersToSheet() err := bc.SyncPaidUsersToSheet()
if err != nil { if err != nil {
log.Printf("Error sync: %s\n", err) log.Printf("Error sync: %s\n", err)
} }
time.Sleep(60 * time.Second) time.Sleep(60 * time.Second)
} }
} }
func notifyAboutEvents(bc BotController) { func notifyAboutEvents(bc BotController) {
// TODO: migrate to tasks system // TODO: migrate to tasks system
for true { for true {
events, _ := bc.GetAllEvents() events, _ := bc.GetAllEvents()
for _, event := range events { for _, event := range events {
delta := event.Date.Sub(time.Now()) delta := event.Date.Sub(time.Now())
if int(math.Floor(delta.Minutes())) == 8 * 60 { // 8 hours if int(math.Floor(delta.Minutes())) == 8*60 { // 8 hours
reservations, _ := bc.GetReservationsByEventID(event.ID) reservations, _ := bc.GetReservationsByEventID(event.ID)
for _, reservation := range reservations { for _, reservation := range reservations {
uid := reservation.UserID uid := reservation.UserID
msg := tgbotapi.NewMessage(uid, bc.GetBotContent("notify_pre_event")) msg := tgbotapi.NewMessage(uid, bc.GetBotContent("notify_pre_event"))
bc.bot.Send(msg) bc.bot.Send(msg)
} }
} }
} }
time.Sleep(60 * time.Second) time.Sleep(60 * time.Second)
} }
} }
func ProcessUpdate(bc BotController, update tgbotapi.Update) { func ProcessUpdate(bc BotController, update tgbotapi.Update) {
@ -104,12 +103,12 @@ func ProcessUpdate(bc BotController, update tgbotapi.Update) {
var UserID = update.Message.From.ID var UserID = update.Message.From.ID
user := bc.GetUser(UserID) user := bc.GetUser(UserID)
bc.LogMessage(update) bc.LogMessage(update)
log.Printf("Surname: %s\n", update.SentFrom().LastName) log.Printf("Surname: %s\n", update.SentFrom().LastName)
bc.UpdateUserInfo(GetUserInfo(update.SentFrom())) bc.UpdateUserInfo(GetUserInfo(update.SentFrom()))
// TODO: REMOVE // TODO: REMOVE
reservation := Reservation{UserID: UserID, EventID: 1, Status: Paid} reservation := Reservation{UserID: UserID, EventID: 1, Status: Paid}
bc.db.Create(&reservation) bc.db.Create(&reservation)
text := update.Message.Text text := update.Message.Text
if strings.HasPrefix(text, "/") { if strings.HasPrefix(text, "/") {
@ -153,13 +152,13 @@ func handleCallbackQuery(bc BotController, update tgbotapi.Update) {
user := bc.GetUser(update.CallbackQuery.From.ID) user := bc.GetUser(update.CallbackQuery.From.ID)
if update.CallbackQuery.Data == "more_info" { if update.CallbackQuery.Data == "more_info" {
sendMessage(bc, update.FromChat().ID, bc.GetBotContent("more_info_text")) sendMessage(bc, update.FromChat().ID, bc.GetBotContent("more_info_text"))
msg := tgbotapi.NewMessage(update.FromChat().ID, bc.GetBotContent("more_info_text")) msg := tgbotapi.NewMessage(update.FromChat().ID, bc.GetBotContent("more_info_text"))
var entities []tgbotapi.MessageEntity var entities []tgbotapi.MessageEntity
meta, _ := bc.GetBotContentMetadata("more_info_text") meta, _ := bc.GetBotContentMetadata("more_info_text")
json.Unmarshal([]byte(meta), &entities) json.Unmarshal([]byte(meta), &entities)
msg.Entities = entities msg.Entities = entities
bc.bot.Send(msg) bc.bot.Send(msg)
} else if user.IsEffectiveAdmin() { } else if user.IsEffectiveAdmin() {
handleAdminCallback(bc, update, user) handleAdminCallback(bc, update, user)
} }
@ -192,19 +191,19 @@ func handleStartCommand(bc BotController, update tgbotapi.Update, user User) {
bc.db.Model(&user).Update("state", "start") bc.db.Model(&user).Update("state", "start")
rows := [][]tgbotapi.InlineKeyboardButton{} rows := [][]tgbotapi.InlineKeyboardButton{}
for _, d := range nearestDates { for _, d := range nearestDates {
k, v := getDateButton(d) k, v := getDateButton(d)
rows = append(rows, rows = append(rows,
tgbotapi.NewInlineKeyboardRow( tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(k, v), tgbotapi.NewInlineKeyboardButtonData(k, v),
), ),
) )
} }
rows = append(rows, rows = append(rows,
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData( tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData(
bc.GetBotContent("more_info"), "more_info", bc.GetBotContent("more_info"), "more_info",
)), )),
) )
kbd := tgbotapi.NewInlineKeyboardMarkup(rows...) kbd := tgbotapi.NewInlineKeyboardMarkup(rows...)
img, err := bc.GetBotContentVerbose("preview_image") img, err := bc.GetBotContentVerbose("preview_image")
if err != nil || img == "" { if err != nil || img == "" {
@ -248,15 +247,17 @@ func handleDeopCommand(bc BotController, update tgbotapi.Update, user User) {
} }
func handleBroadcastCommand(bc BotController, update tgbotapi.Update, user User) { func handleBroadcastCommand(bc BotController, update tgbotapi.Update, user User) {
if !user.IsAdmin() {return} if !user.IsAdmin() {
return
}
var users []User var users []User
bc.db.Find(&users) bc.db.Find(&users)
for _, user := range users { for _, user := range users {
user = user user = user
// TODO!!! // TODO!!!
} }
} }
func handleDefaultMessage(bc BotController, update tgbotapi.Update, user User) { func handleDefaultMessage(bc BotController, update tgbotapi.Update, user User) {
@ -316,8 +317,8 @@ func handleDefaultMessage(bc BotController, update tgbotapi.Update, user User) {
} else if strings.HasPrefix(user.State, "stringset:") { } else if strings.HasPrefix(user.State, "stringset:") {
Literal := strings.Split(user.State, ":")[1] Literal := strings.Split(user.State, ":")[1]
b, _ := json.Marshal(update.Message.Entities) b, _ := json.Marshal(update.Message.Entities)
strEntities := string(b) strEntities := string(b)
bc.SetBotContent(Literal, update.Message.Text, strEntities) bc.SetBotContent(Literal, update.Message.Text, strEntities)
bc.db.Model(&user).Update("state", "start") bc.db.Model(&user).Update("state", "start")
@ -448,18 +449,18 @@ func getAdmins(bc BotController) []User {
func getDateButton(date time.Time) (string, string) { func getDateButton(date time.Time) (string, string) {
// Format the date as needed, e.g., "2006-01-02" // Format the date as needed, e.g., "2006-01-02"
wday := WeekLabels[int(date.Local().Weekday())] wday := WeekLabels[int(date.Local().Weekday())]
formattedDate := strings.Join([]string{ formattedDate := strings.Join([]string{
date.Format("02.01.2006"), date.Format("02.01.2006"),
"(" + wday + ")", "(" + wday + ")",
"в", "в",
date.Format("15:04"), date.Format("15:04"),
}, " ") }, " ")
// Create a token similar to what GetBotContent accepts // Create a token similar to what GetBotContent accepts
token := fmt.Sprintf("date_%s", date.Format("200601021504")) // Example token format token := fmt.Sprintf("date_%s", date.Format("200601021504")) // Example token format
return strings.Join([]string{"Пойду", formattedDate}, " "), token return strings.Join([]string{"Пойду", formattedDate}, " "), token
} }
func GetUserInfo(user *tgbotapi.User) UserInfo { func GetUserInfo(user *tgbotapi.User) UserInfo {

View File

@ -11,8 +11,8 @@ var assets = map[string]string{
"Просьба подписаться на канал": "subscribe_message", "Просьба подписаться на канал": "subscribe_message",
"Ссылка на канал": "channel_link", "Ссылка на канал": "channel_link",
"Подробнее о мероприятии": "more_info", "Подробнее о мероприятии": "more_info",
"Текст о мероприятии": "more_info_text", "Текст о мероприятии": "more_info_text",
"Текст: напоминание за 8 часов": "notify_pre_event", "Текст: напоминание за 8 часов": "notify_pre_event",
} }
func handlePanel(bc BotController, user User) { func handlePanel(bc BotController, user User) {

View File

@ -8,19 +8,19 @@ import (
) )
type Config struct { type Config struct {
BotToken string `env:"BOTTOKEN, required"` BotToken string `env:"BOTTOKEN, required"`
AdminPass string `env:"ADMINPASSWORD, required"` // to activate admin privileges in bot type command: /secret `AdminPass` AdminPass string `env:"ADMINPASSWORD, required"` // to activate admin privileges in bot type command: /secret `AdminPass`
AdminID *int64 `env:"ADMINID"` // optional admin ID for notifications AdminID *int64 `env:"ADMINID"` // optional admin ID for notifications
SheetID string `env:"SHEETID, required"` // id of google sheet where users will be synced SheetID string `env:"SHEETID, required"` // id of google sheet where users will be synced
} }
func GetConfig() Config { func GetConfig() Config {
ctx := context.Background() ctx := context.Background()
var c Config var c Config
if err := envconfig.Process(ctx, &c); err != nil { if err := envconfig.Process(ctx, &c); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return c return c
} }