Compare commits

...

50 Commits

Author SHA1 Message Date
Akulij
2abb18cb49 get events for start buttons from database instead of hardcoded ones
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
2025-05-05 19:41:11 +03:00
Akulij
0949b1c812 create notifyPaid function
sends in support chat info about user that just paid
2025-05-05 19:39:08 +03:00
Akulij
22c2b63069 fix: return reservedate: callback in getDatebutton 2025-05-05 19:38:15 +03:00
Akulij
04d8e8f086 define const seatscnt for max available bookings on event 2025-05-05 19:35:24 +03:00
Akulij
b0137e83fc handle user state enternamereservation 2025-05-05 19:33:43 +03:00
Akulij
feb5a67d6a handle reservedate: callback 2025-05-05 19:32:55 +03:00
Akulij
d92e4899c8 handle paidcallback: callback 2025-05-05 19:32:23 +03:00
Akulij
8352829b92 fix WeekLabels sunday's name 2025-05-05 19:31:19 +03:00
Akulij
c78c1fe295 format hardcoded assets table 2025-05-05 19:29:43 +03:00
Akulij
81cecbaaf6 write reservation status column in gsheets 2025-05-05 19:27:53 +03:00
Akulij
065f4fffbf create bc reservation methods 2025-05-05 19:27:08 +03:00
Akulij
92dd0bbb2d add support of text formating for /start message 2025-03-29 19:35:25 +08:00
Akulij
201437d6c2 fix: double send of more_info 2025-03-29 19:35:07 +08:00
Akulij
bf1d7df88f asynchronously send event notification 2025-03-29 19:34:18 +08:00
Akulij
5923f6bf3f replace math.Floor with math.Ceil
it's better to notify user at 18:00, not 17:59
2025-03-29 19:22:57 +08:00
Akulij
ed99f4a16f go fmt 2025-03-29 19:19:47 +08:00
Akulij
8b6317b31a update main file 2025-03-29 19:19:12 +08:00
Akulij
ce608abff1 add Metadata field in BotContent table 2025-03-29 19:18:23 +08:00
Akulij
95e92dea3c create UserInfo table 2025-03-29 19:17:58 +08:00
Akulij
b6dae4b24c migrate all tables 2025-03-29 19:16:58 +08:00
Akulij
098ef11ca3 create reservation and event table 2025-03-29 19:16:00 +08:00
Akulij
0564467c59 create tasks table 2025-03-29 19:15:11 +08:00
Akulij
9f923a04fa add more required asset entries to panel 2025-03-29 19:13:17 +08:00
Akulij
319a4b0f66 add SheetID to config structure 2025-03-29 19:12:29 +08:00
Akulij
84b59379d7 export SHEETID env var used in config 2025-03-29 19:12:03 +08:00
Akulij
2134659426 add google sheets api as a dependency 2025-03-29 19:11:37 +08:00
Akulij
96af28b365 create google sheets manipulation file 2025-03-29 19:11:03 +08:00
Akulij
4c1e4180b7 ignore credentials.json 2025-03-29 19:10:44 +08:00
Akulij
b7c01addcf go fmt 2025-03-28 15:48:57 +08:00
Akulij
61214ca405 add possibilities info to asset handler 2025-03-28 15:32:12 +08:00
Akulij
14e250bfc3 move out panel logic into panel file 2025-03-27 23:11:46 +08:00
Akulij
dff3fc58ad create telegram utilities file 2025-03-27 23:11:27 +08:00
Akulij
c785e3676c add hardcoded comming dates of meetings
WHY??? it is a way to develop bot in a few days :)
2025-03-27 22:43:54 +08:00
Akulij
b94873b6a8 move out assets for admin to panel file 2025-03-27 22:43:21 +08:00
Akulij
722431e8a5 create asset map for admin panel 2025-03-27 22:35:05 +08:00
Akulij
e7b21a45b3 add /deop admin command 2025-03-27 22:34:42 +08:00
Akulij
ee254ed865 fix: non-admin weren't able to use secret command 2025-03-27 22:32:10 +08:00
Akulij
121022fb54 check if user is admin in panel callback 2025-03-27 22:24:58 +08:00
Akulij
a00b7e874b fix missing argument in admin handlers 2025-03-27 22:05:21 +08:00
Akulij
ecd55a3031 improve panel handler security
if later it will apear somewhere else that admin block
2025-03-27 22:02:14 +08:00
Akulij
59d7f07f48 map secret and panel command handlers 2025-03-27 22:01:34 +08:00
Akulij
8a937724c1 organise better message handling 2025-03-27 21:59:04 +08:00
Akulij
94894b2c88 delete message counter because messages table now exists 2025-03-27 21:24:49 +08:00
Akulij
5144cb58dc create messages table and logger 2025-03-27 21:19:41 +08:00
Akulij
38c7bef71c go fmt 2025-03-27 20:54:33 +08:00
Akulij
4c0d2ec3a3 move out new user declaration into db file 2025-03-27 20:53:06 +08:00
Akulij
b770b2c5dc move out bot controller logic into separate file 2025-03-27 20:44:20 +08:00
Akulij
c81e781247 add admin commands map to handlers 2025-03-27 20:41:41 +08:00
Akulij
c87e4683db log message entities 2025-03-27 20:23:17 +08:00
Akulij
ce7c1613b1 move prints from fmt to log module 2025-03-27 20:22:58 +08:00
11 changed files with 790 additions and 143 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.env
**.jpg
**.db
credentials.json

62
cmd/app/botcontroller.go Normal file
View File

@ -0,0 +1,62 @@
package main
import (
"errors"
"log"
"gorm.io/gorm"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/akulij/ticketbot/config"
)
type BotController struct {
cfg config.Config
bot *tgbotapi.BotAPI
db *gorm.DB
updates tgbotapi.UpdatesChannel
}
func GetBotController() BotController {
cfg := config.GetConfig()
log.Printf("Token value: '%v'\n", cfg.BotToken)
log.Printf("Admin password: '%v'\n", cfg.AdminPass)
log.Printf("Admin ID: '%v'\n", *cfg.AdminID)
bot, err := tgbotapi.NewBotAPI(cfg.BotToken)
if err != nil {
log.Panic(err)
}
db, err := GetDB()
if err != nil {
log.Panic(err)
}
bot.Debug = true // set true only while development, should be set to false in production
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
return BotController{cfg: cfg, bot: bot, db: db, updates: updates}
}
func (bc BotController) LogMessage(update tgbotapi.Update) error {
var msg *tgbotapi.Message
if update.Message != nil {
msg = update.Message
} else {
return errors.New("invalid update provided to message logger")
}
var UserID = msg.From.ID
bc.LogMessageRaw(UserID, msg.Text, msg.Time())
return nil
}

View File

@ -2,6 +2,8 @@ package main
import (
"errors"
"log"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@ -11,10 +13,26 @@ type User struct {
gorm.Model
ID int64
State string
MsgCounter uint
RoleBitmask uint
}
func (bc BotController) GetUserByID(UserID int64) (User, error) {
var user User
bc.db.First(&user, "ID", UserID)
if user == (User{}) {
return User{}, errors.New("No content")
}
return user, nil
}
type UserInfo struct {
gorm.Model
ID int64
Username string
FirstName string
LastName string
}
func (u User) IsAdmin() bool {
return u.RoleBitmask&1 == 1
}
@ -25,8 +43,9 @@ func (u User) IsEffectiveAdmin() bool {
type BotContent struct {
gorm.Model
Literal string
Content string
Literal string
Content string
Metadata string
}
func GetDB() (*gorm.DB, error) {
@ -36,7 +55,12 @@ func GetDB() (*gorm.DB, error) {
}
db.AutoMigrate(&User{})
db.AutoMigrate(&UserInfo{})
db.AutoMigrate(&BotContent{})
db.AutoMigrate(&Message{})
db.AutoMigrate(&Reservation{})
db.AutoMigrate(&Event{})
db.AutoMigrate(&Task{})
return db, err
}
@ -55,9 +79,201 @@ func (bc BotController) GetBotContent(Literal string) string {
return content
}
func (bc BotController) SetBotContent(Literal string, Content string) {
c := BotContent{Literal: Literal, Content: Content}
bc.db.FirstOrCreate(&c, "Literal", Literal)
bc.db.Model(&c).Update("Content", Content)
func (bc BotController) GetBotContentMetadata(Literal string) (string, error) {
var c BotContent
bc.db.First(&c, "Literal", Literal)
if c == (BotContent{}) {
return "[]", errors.New("No metadata")
}
return c.Metadata, nil
}
func (bc BotController) SetBotContent(Literal string, Content string, Metadata string) {
c := BotContent{Literal: Literal, Content: Content, Metadata: Metadata}
bc.db.FirstOrCreate(&c, "Literal", Literal)
bc.db.Model(&c).Update("Content", Content)
bc.db.Model(&c).Update("Metadata", Metadata)
}
func (bc BotController) GetUser(UserID int64) User {
var user User
bc.db.First(&user, "id", UserID)
if user == (User{}) {
log.Printf("New user: [%d]", UserID)
user = User{ID: UserID, State: "start"}
bc.db.Create(&user)
}
return user
}
func (bc BotController) UpdateUserInfo(ui UserInfo) {
bc.db.Save(&ui)
}
func (bc BotController) GetUserInfo(UserID int64) (UserInfo, error) {
var ui UserInfo
bc.db.First(&ui, "ID", UserID)
if ui == (UserInfo{}) {
log.Printf("NO UserInfo FOUND!!!, id: [%d]", UserID)
return UserInfo{}, errors.New("NO UserInfo FOUND!!!")
}
return ui, nil
}
type Message struct {
gorm.Model
UserID int64
Msg string
Datetime *time.Time
}
func (bc BotController) LogMessageRaw(UserID int64, Msg string, Time time.Time) {
msg := Message{
UserID: UserID,
Msg: Msg,
Datetime: &Time,
}
bc.db.Create(&msg)
}
type ReservationStatus int64
const (
Booked ReservationStatus = iota
Paid
)
var ReservationStatusString = []string{
"Забронировано",
"Оплачено",
}
type Reservation struct {
gorm.Model
ID int64 `gorm:"primary_key"`
UserID int64 `gorm:"uniqueIndex:user_event_uniq"`
EnteredName string
TimeBooked *time.Time
EventID int64 `gorm:"uniqueIndex:user_event_uniq"`
Status ReservationStatus
}
func (bc BotController) GetAllReservations() ([]Reservation, error) {
var reservations []Reservation
result := bc.db.Find(&reservations)
if result.Error != nil {
return nil, result.Error
}
return reservations, nil
}
func (bc BotController) GetReservationsByEventID(EventID int64) ([]Reservation, error) {
var reservations []Reservation
result := bc.db.Where("event_id = ?", EventID).Find(&reservations)
if result.Error != nil {
return nil, result.Error
}
return reservations, nil
}
func (bc BotController) CountReservationsByEventID(EventID int64) (int64, error) {
var count int64
result := bc.db.Model(&Reservation{}).Where("event_id = ?", EventID).Count(&count)
if result.Error != nil {
return 0, result.Error
}
return count, nil
}
func (bc BotController) CreateReservation(userID int64, eventID int64, name string) (Reservation, error) {
var dubaiLocation, _ = time.LoadLocation("Asia/Dubai")
timenow := time.Now().In(dubaiLocation)
reservation := Reservation{
UserID: userID,
EventID: eventID,
TimeBooked: &timenow,
Status: Booked,
EnteredName: name,
}
result := bc.db.Create(&reservation)
return reservation, result.Error
}
func (bc BotController) GetReservationByID(reservationID int64) (Reservation, error) {
var reservation Reservation
result := bc.db.First(&reservation, reservationID)
if result.Error != nil {
return Reservation{}, result.Error
}
return reservation, nil
}
func (bc BotController) UpdateReservation(r Reservation) {
bc.db.Save(&r)
}
type Event struct {
gorm.Model
ID int64 `gorm:"primary_key"`
Date *time.Time `gorm:"unique"`
}
func (bc BotController) GetAllEvents() ([]Event, error) {
var events []Event
result := bc.db.Find(&events)
if result.Error != nil {
return nil, result.Error
}
return events, nil
}
func (bc BotController) GetEvent(EventID int64) (Event, error) {
var event Event
result := bc.db.First(&event, EventID)
if result.Error != nil {
return Event{}, result.Error
}
return event, nil
}
type TaskType int64
const (
SyncSheet TaskType = iota
NotifyAboutEvent
)
type Task struct {
gorm.Model
ID int64 `gorm:"primary_key"`
Type TaskType
EventID int64
}
func (bc BotController) CreateSimpleTask(taskType TaskType) error {
task := Task{
Type: taskType,
}
return bc.CreateTask(task)
}
func (bc BotController) CreateTask(task Task) error {
result := bc.db.Create(&task)
return result.Error
}
func (bc BotController) DeleteTask(taskID int64) error {
result := bc.db.Delete(&Task{}, taskID)
return result.Error
}
func (bc BotController) GetAllTasks() ([]Task, error) {
var tasks []Task
result := bc.db.Find(&tasks)
if result.Error != nil {
return nil, result.Error
}
return tasks, nil
}

61
cmd/app/gsheets.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"context"
"fmt"
"log"
"time"
"google.golang.org/api/option"
"google.golang.org/api/sheets/v4"
)
func (bc *BotController) SyncPaidUsersToSheet() error {
reservations, _ := bc.GetAllReservations()
ctx := context.Background()
srv, err := sheets.NewService(ctx,
option.WithCredentialsFile("./credentials.json"),
option.WithScopes(sheets.SpreadsheetsScope),
)
if err != nil {
return fmt.Errorf("unable to retrieve Sheets client: %v", err)
}
var values [][]interface{}
values = append(values, []interface{}{"Телеграм ID", "Имя", "Фамилия", "Никнейм", "Указанное имя", "Дата", "Телефон", "Статус"})
for _, reservation := range reservations {
if reservation.Status != Paid {
continue
}
uid := reservation.UserID
user, _ := bc.GetUserByID(uid)
ui, _ := bc.GetUserInfo(uid)
event, _ := bc.GetEvent(reservation.EventID)
status := ReservationStatusString[reservation.Status]
values = append(values, []interface{}{user.ID, ui.FirstName, ui.LastName, ui.Username, reservation.EnteredName, formatDate(event.Date), "", status})
}
// Prepare the data to be written to the sheet
valueRange := &sheets.ValueRange{
Values: values,
}
_, err = srv.Spreadsheets.Values.Clear(bc.cfg.SheetID, "A1:Z1000", &sheets.ClearValuesRequest{}).Do()
// Write the data to the specified range in the sheet
_, err = srv.Spreadsheets.Values.Update(bc.cfg.SheetID, "A1", valueRange).ValueInputOption("RAW").Do()
if err != nil {
return fmt.Errorf("unable to write data to sheet: %v", err)
}
log.Printf("Successfully synced %d reservations to the Google Sheet.", len(reservations))
return nil
}
func formatDate(t *time.Time) string {
return t.Format("02.01 15:04")
}

View File

@ -1,67 +1,124 @@
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"
"github.com/akulij/ticketbot/config"
"gorm.io/gorm"
)
type BotController struct {
cfg config.Config
bot *tgbotapi.BotAPI
db *gorm.DB
updates tgbotapi.UpdatesChannel
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
}
func GetBotController() BotController {
cfg := config.GetConfig()
fmt.Printf("Token value: '%v'\n", cfg.BotToken)
fmt.Printf("Admin password: '%v'\n", cfg.AdminPass)
fmt.Printf("Admin ID: '%v'\n", *cfg.AdminID)
var dubaiLocation, _ = time.LoadLocation("Asia/Dubai")
bot, err := tgbotapi.NewBotAPI(cfg.BotToken)
if err != nil {
log.Panic(err)
}
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),
}
db, err := GetDB()
if err != nil {
log.Panic(err)
}
const seatscnt = 10
bot.Debug = true // set true only while development, should be set to false in production
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
return BotController{cfg: cfg, bot: bot, db: db, updates: updates}
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 {
handleMessage(bc, update)
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 {
@ -69,47 +126,76 @@ func ProcessUpdate(bc BotController, update tgbotapi.Update) {
}
}
func handleMessage(bc BotController, update tgbotapi.Update) {
var UserID = update.Message.From.ID
func handleCommand(bc BotController, update tgbotapi.Update, user User) {
msg := update.Message
var user User
bc.db.First(&user, "id", UserID)
if user == (User{}) {
log.Printf("New user: [%d]", UserID)
user = User{ID: UserID, State: "start"}
bc.db.Create(&user)
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
}
}
bc.db.Model(&user).Update("MsgCounter", user.MsgCounter+1)
log.Printf("User[%d] messages: %d", user.ID, user.MsgCounter)
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
possibleCommand := strings.Split(update.Message.Text, " ")[0]
args := strings.Split(update.Message.Text, " ")[1:]
log.Printf("Args: %s", args)
switch {
case possibleCommand == "/start":
// commands for non-admins
switch command {
case "/start":
handleStartCommand(bc, update, user)
case possibleCommand == "/id" && user.IsAdmin():
bc.bot.Send(tgbotapi.NewMessage(update.Message.Chat.ID, strconv.FormatInt(update.Message.Chat.ID, 10)))
case possibleCommand == "/secret" && len(args) > 0 && args[0] == bc.cfg.AdminPass:
case "/secret":
handleSecretCommand(bc, update, user)
case possibleCommand == "/panel" && user.IsAdmin():
handlePanelCommand(bc, update, user)
case possibleCommand == "/usermode" && user.IsEffectiveAdmin():
handleUserModeCommand(bc, update, user)
default:
handleDefaultMessage(bc, update, user)
}
}
func handleCallbackQuery(bc BotController, update tgbotapi.Update) {
var user User
bc.db.First(&user, "id", update.CallbackQuery.From.ID)
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 strings.HasPrefix(update.CallbackQuery.Data, "paidcallback:") {
token := strings.Split(update.CallbackQuery.Data, ":")[1]
reservationid, err := strconv.ParseInt(token, 10, 64)
if err != nil {
log.Printf("Error parsing reservation token: %s\n", err)
return
}
reservation, _ := bc.GetReservationByID(reservationid)
reservation.Status = Paid
bc.UpdateReservation(reservation)
notifyPaid(bc, reservation)
sendMessage(bc, update.CallbackQuery.From.ID, bc.GetBotContent("post_payment_message"))
} else if strings.HasPrefix(update.CallbackQuery.Data, "reservedate:") {
datetoken := strings.Split(update.CallbackQuery.Data, ":")[1]
eventid, err := strconv.ParseInt(datetoken, 10, 64)
if err != nil {
log.Printf("Error parsing date token: %s\n", err)
return
}
taken, _ := bc.CountReservationsByEventID(eventid)
if taken >= seatscnt {
sendMessage(bc, user.ID, bc.GetBotContent("soldout_message"))
return
}
// event, _ := bc.GetEvent(eventid)
reservation, err := bc.CreateReservation(update.CallbackQuery.From.ID, eventid, "Не указано")
if err != nil {
log.Printf("Error creating reservation: %s\n", err)
return
}
bc.db.Model(&user).Update("state", "enternamereservation:"+strconv.FormatInt(reservation.ID, 10))
sendMessage(bc, user.ID, bc.GetBotContent("reserved_message"))
if update.CallbackQuery.Data == "leave_ticket_button" {
handleLeaveTicketButton(bc, update, user)
} else if user.IsEffectiveAdmin() {
handleAdminCallback(bc, update, user)
}
@ -125,7 +211,7 @@ func handleCallbackQuery(bc BotController, update tgbotapi.Update) {
func handleChannelPost(bc BotController, update tgbotapi.Update) {
post := update.ChannelPost
if post.Text == "setchannelid" {
bc.SetBotContent("channelid", strconv.FormatInt(post.SenderChat.ID, 10))
bc.SetBotContent("channelid", strconv.FormatInt(post.SenderChat.ID, 10), "")
var admins []User
bc.db.Where("role_bitmask & 1 = ?", 1).Find(&admins)
@ -140,63 +226,67 @@ func handleChannelPost(bc BotController, update tgbotapi.Update) {
// Helper functions for specific commands
func handleStartCommand(bc BotController, update tgbotapi.Update, user User) {
bc.db.Model(&user).Update("state", "start")
kbd := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(bc.GetBotContent("leave_ticket_button"), "leave_ticket_button"),
),
)
if user.IsAdmin() {
kbd = tgbotapi.NewInlineKeyboardMarkup(
rows := [][]tgbotapi.InlineKeyboardButton{}
events, _ := bc.GetAllEvents()
for _, event := range events {
if event.Date.Sub(time.Now()) < 2*time.Hour {
continue
}
k, _ := getDateButton(*event.Date)
taken, _ := bc.CountReservationsByEventID(event.ID)
k = strings.Join([]string{
k,
"(" + strconv.FormatInt(taken, 10) + "/" + strconv.Itoa(seatscnt) + ")",
}, " ")
if taken == seatscnt {
k += " (Распродано)"
}
token := "reservedate:" + strconv.FormatInt(event.ID, 10)
rows = append(rows,
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(bc.GetBotContent("leave_ticket_button"), "leave_ticket_button"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Panel", "panel"),
tgbotapi.NewInlineKeyboardButtonData(k, token),
),
)
}
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.ParseMode = "markdown"
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) {
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)
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) {
if !user.IsEffectiveAdmin() {
bc.db.Model(&user).Update("RoleBitmask", user.RoleBitmask|0b10)
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "You was in usermode, turned back to admin mode...")
bc.bot.Send(msg)
}
kbd := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Стартовая картинка", "update:preview_image")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Приветственный текст", "update:start")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Кнопка для заявки", "update:leave_ticket_button")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("ID чата", "update:supportchatid")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("ID канала", "update:channelid")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Уведомление об отправке тикета", "update:sended_notify")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Просьба оставить тикет", "update:leaveticket_message")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Просьба подписаться на канал", "update:subscribe_message")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Ссылка на канал", "update:channel_link")),
)
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Выберите пункт для изменения")
msg.ReplyMarkup = kbd
bc.bot.Send(msg)
handlePanel(bc, user)
}
func handleUserModeCommand(bc BotController, update tgbotapi.Update, user User) {
@ -206,6 +296,27 @@ func handleUserModeCommand(bc BotController, update tgbotapi.Update, user User)
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
@ -239,6 +350,19 @@ func handleDefaultMessage(bc BotController, update tgbotapi.Update, user User) {
bc.db.Model(&user).Update("state", "start")
msg := tgbotapi.NewMessage(update.Message.Chat.ID, bc.GetBotContent("sended_notify"))
bc.bot.Send(msg)
} else if strings.HasPrefix(user.State, "enternamereservation:") {
resstr := strings.Split(user.State, ":")[1]
reservationid, _ := strconv.ParseInt(resstr, 10, 64)
reservation, _ := bc.GetReservationByID(reservationid)
reservation.EnteredName = update.Message.Text
nd := time.Now().In(dubaiLocation)
reservation.TimeBooked = &nd
bc.UpdateReservation(reservation)
sendMessageKeyboard(bc, user.ID, bc.GetBotContent("ask_to_pay"),
generateTgInlineKeyboard(map[string]string{"ТЕСТ оплачено": "paidcallback:" + strconv.FormatInt(reservationid, 10)}),
)
} else if user.IsEffectiveAdmin() {
if user.State != "start" {
if strings.HasPrefix(user.State, "imgset:") {
@ -246,7 +370,7 @@ func handleDefaultMessage(bc BotController, update tgbotapi.Update, user User) {
if update.Message.Text == "unset" {
var l BotContent
bc.db.First(&l, "Literal", Literal)
bc.SetBotContent(Literal, "")
bc.SetBotContent(Literal, "", "")
}
maxsize := 0
fileid := ""
@ -256,13 +380,17 @@ func handleDefaultMessage(bc BotController, update tgbotapi.Update, user User) {
maxsize = p.FileSize
}
}
bc.SetBotContent(Literal, fileid)
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]
bc.SetBotContent(Literal, update.Message.Text)
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)
@ -334,30 +462,12 @@ func handleAdminCallback(bc BotController, update tgbotapi.Update, user User) {
} 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))"))
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) {
if !user.IsEffectiveAdmin() {
bc.db.Model(&user).Update("RoleBitmask", user.RoleBitmask|0b10)
msg := tgbotapi.NewMessage(user.ID, "You was in usermode, turned back to admin mode...")
bc.bot.Send(msg)
}
kbd := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Стартовая картинка", "update:preview_image")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Приветственный текст", "update:start")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Кнопка для заявки", "update:leave_ticket_button")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("ID чата", "update:supportchatid")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("ID канала", "update:channelid")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Уведомление об отправке тикета", "update:sended_notify")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Просьба оставить тикет", "update:leaveticket_message")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Просьба подписаться на канал", "update:subscribe_message")),
tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("Ссылка на канал", "update:channel_link")),
)
msg := tgbotapi.NewMessage(user.ID, "Выберите пункт для изменения")
msg.ReplyMarkup = kbd
bc.bot.Send(msg)
handlePanel(bc, user)
}
func DownloadFile(filepath string, url string) error {
@ -382,16 +492,67 @@ func DownloadFile(filepath string, url string) error {
}
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
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("reservedate:%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,
}
}
func notifyPaid(bc BotController, reservation Reservation) {
chatidstr := bc.GetBotContent("supportchatid")
chatid, _ := strconv.ParseInt(chatidstr, 10, 64)
ui, _ := bc.GetUserInfo(reservation.UserID)
event, _ := bc.GetEvent(reservation.EventID)
msg := fmt.Sprintf(
"Пользователь %s (%s) оплатил на %s",
ui.FirstName,
ui.Username,
event.Date.Format("02.01 15:04"),
)
bc.bot.Send(tgbotapi.NewMessage(chatid, msg))
}

33
cmd/app/panel.go Normal file
View File

@ -0,0 +1,33 @@
package main
var assets = map[string]string{
"Стартовая картинка": "preview_image",
"Приветственный текст": "start",
"Кнопка для заявки": "leave_ticket_button",
"ID чата": "supportchatid",
"ID канала": "channelid",
"Уведомление об отправке тикета": "sended_notify",
"Просьба оставить тикет": "leaveticket_message",
"Просьба подписаться на канал": "subscribe_message",
"Ссылка на канал": "channel_link",
"Подробнее о мероприятии": "more_info",
"Текст о мероприятии": "more_info_text",
"Текст: напоминание за 8 часов": "notify_pre_event",
"Текст: забронированно и ввести имя": "reserved_message",
"Текст: после имени на оплату": "ask_to_pay",
"Текст: распродано": "soldout_message",
"Текст: После оплаты": "post_payment_message",
}
func handlePanel(bc BotController, user User) {
if !user.IsAdmin() {
return
}
if !user.IsEffectiveAdmin() {
bc.db.Model(&user).Update("RoleBitmask", user.RoleBitmask|0b10)
sendMessage(bc, user.ID, "You was in usermode, turned back to admin mode...")
}
m := Map(assets, func(v string) string { return "update:" + v })
kbd := generateTgInlineKeyboard(m)
sendMessageKeyboard(bc, user.ID, "Выберите пункт для изменения", kbd)
}

37
cmd/app/tghelper.go Normal file
View File

@ -0,0 +1,37 @@
package main
import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func Map[K comparable, T, V any](ts map[K]T, fn func(T) V) map[K]V {
result := make(map[K]V, len(ts))
for k, t := range ts {
result[k] = fn(t)
}
return result
}
func generateTgInlineKeyboard(buttonsCallback map[string]string) tgbotapi.InlineKeyboardMarkup {
rows := [][]tgbotapi.InlineKeyboardButton{}
for k, v := range buttonsCallback {
rows = append(rows,
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(k, v),
),
)
}
return tgbotapi.NewInlineKeyboardMarkup(rows...)
}
func sendMessage(bc BotController, UserID int64, Msg string) {
msg := tgbotapi.NewMessage(UserID, Msg)
bc.bot.Send(msg)
}
func sendMessageKeyboard(bc BotController, UserID int64, Msg string, Kbd tgbotapi.InlineKeyboardMarkup) {
msg := tgbotapi.NewMessage(UserID, Msg)
msg.ReplyMarkup = Kbd
bc.bot.Send(msg)
}

View File

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

29
go.mod
View File

@ -1,14 +1,39 @@
module github.com/akulij/ticketbot
go 1.22.2
go 1.23.0
toolchain go1.24.0
require (
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/sethvargo/go-envconfig v1.0.1 // indirect
golang.org/x/text v0.14.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/api v0.228.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gorm.io/driver/sqlite v1.5.6 // indirect
gorm.io/gorm v1.25.11 // indirect
)

49
go.sum
View File

@ -1,5 +1,26 @@
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -8,8 +29,36 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/sethvargo/go-envconfig v1.0.1 h1:9wglip/5fUfaH0lQecLM8AyOClMw0gT0A9K2c2wozao=
github.com/sethvargo/go-envconfig v1.0.1/go.mod h1:OKZ02xFaD3MvWBBmEW45fQr08sJEsonGrrOdicvQmQA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=

1
run.sh
View File

@ -1,4 +1,5 @@
source ./.env
export BOTTOKEN
export ADMINPASSWORD
export SHEETID
go run ./cmd/app