package tickets import ( "context" "errors" "fmt" "io" "sync" "time" "velox-bot/internal/db/repos/ticketrepo" "github.com/bwmarrin/discordgo" ) // fakeStore is an in-memory ticketrepo with the same rules as the SQL. type fakeStore struct { mu sync.Mutex settings map[int64]*ticketrepo.Settings cats []ticketrepo.Category tickets []*ticketrepo.Ticket next map[int64]int cmds []ticketrepo.Command done []int64 } func newStore() *fakeStore { return &fakeStore{settings: map[int64]*ticketrepo.Settings{}, next: map[int64]int{}} } func (f *fakeStore) configure(guild, channel, logCh int64) { f.settings[guild] = &ticketrepo.Settings{ GuildID: guild, Enabled: true, ChannelID: channel, LogChannelID: logCh, PanelTitle: ticketrepo.DefaultPanelTitle, PanelDescription: ticketrepo.DefaultPanelDescription, MaxOpenPerUser: 1, Configured: true, } } func (f *fakeStore) addCat(c ticketrepo.Category) ticketrepo.Category { c.ID = int64(len(f.cats) + 1) f.cats = append(f.cats, c) return c } func (f *fakeStore) GetSettings(_ context.Context, g int64) (*ticketrepo.Settings, error) { if s, ok := f.settings[g]; ok { cp := *s return &cp, nil } return &ticketrepo.Settings{GuildID: g, Enabled: true, PanelTitle: ticketrepo.DefaultPanelTitle, MaxOpenPerUser: 1}, nil } func (f *fakeStore) SetPanelMessage(_ context.Context, g, m int64) error { f.settings[g].PanelMessageID = m return nil } func (f *fakeStore) NextNumber(_ context.Context, g int64) (int, error) { f.next[g]++ return f.next[g], nil } func (f *fakeStore) ListCategories(_ context.Context, g int64) ([]ticketrepo.Category, error) { var out []ticketrepo.Category for _, c := range f.cats { if c.GuildID == g { out = append(out, c) } } return out, nil } func (f *fakeStore) GetCategory(_ context.Context, g, id int64) (*ticketrepo.Category, error) { for _, c := range f.cats { if c.GuildID == g && c.ID == id { cp := c return &cp, nil } } return nil, nil } func (f *fakeStore) CountOpenByOpener(_ context.Context, g, u int64) (int, error) { n := 0 for _, t := range f.tickets { if t.GuildID == g && t.OpenerID == u && t.Status == ticketrepo.StatusOpen { n++ } } return n, nil } func (f *fakeStore) CreateTicket(_ context.Context, t ticketrepo.Ticket) (int64, error) { t.ID = int64(len(f.tickets) + 1) t.Status = ticketrepo.StatusOpen t.CreatedAt = time.Now().Add(-90 * time.Minute) f.tickets = append(f.tickets, &t) return t.ID, nil } func (f *fakeStore) byThread(th int64) *ticketrepo.Ticket { for _, t := range f.tickets { if t.ThreadID == th { return t } } return nil } func (f *fakeStore) GetTicketByThread(_ context.Context, th int64) (*ticketrepo.Ticket, error) { if t := f.byThread(th); t != nil { cp := *t return &cp, nil } return nil, nil } func (f *fakeStore) Claim(_ context.Context, th, staff int64) (int64, error) { t := f.byThread(th) if t == nil || t.Status != ticketrepo.StatusOpen { return 0, nil } if t.ClaimedBy == 0 { t.ClaimedBy = staff } return t.ClaimedBy, nil } func (f *fakeStore) Close(_ context.Context, th, by int64, reason string) (bool, error) { t := f.byThread(th) if t == nil || t.Status != ticketrepo.StatusOpen { return false, nil } t.Status, t.ClosedBy, t.CloseReason, t.ClosedAt = ticketrepo.StatusClosed, by, reason, time.Now() return true, nil } func (f *fakeStore) ListUnprocessedCommands(context.Context) ([]ticketrepo.Command, error) { var out []ticketrepo.Command for _, c := range f.cmds { done := false for _, d := range f.done { done = done || d == c.ID } if !done { out = append(out, c) } } return out, nil } func (f *fakeStore) MarkCommandProcessed(_ context.Context, id int64) error { f.done = append(f.done, id) return nil } // fakeDiscord records what the flows ask Discord to do. type fakeDiscord struct { mu sync.Mutex nextID int threads []*discordgo.ThreadStart members map[string][]string // thread -> user IDs sent map[string][]*discordgo.MessageSend edits []*discordgo.MessageEdit chEdits map[string]*discordgo.ChannelEdit deleted []string files map[string]string // file name -> contents history map[string][]*discordgo.Message messages map[string]*discordgo.Message // by "channel/message" failSend error failEdit error failFetch error } func newDiscord() *fakeDiscord { return &fakeDiscord{ nextID: 1000, members: map[string][]string{}, sent: map[string][]*discordgo.MessageSend{}, chEdits: map[string]*discordgo.ChannelEdit{}, files: map[string]string{}, history: map[string][]*discordgo.Message{}, messages: map[string]*discordgo.Message{}, } } func (f *fakeDiscord) newID() string { f.nextID++ return fmt.Sprint(f.nextID) } func (f *fakeDiscord) ThreadStartComplex(ch string, data *discordgo.ThreadStart, _ ...discordgo.RequestOption) (*discordgo.Channel, error) { f.threads = append(f.threads, data) return &discordgo.Channel{ID: f.newID(), ParentID: ch, Name: data.Name, Type: data.Type}, nil } func (f *fakeDiscord) ThreadMemberAdd(th, u string, _ ...discordgo.RequestOption) error { f.members[th] = append(f.members[th], u) return nil } func (f *fakeDiscord) ThreadMemberRemove(th, u string, _ ...discordgo.RequestOption) error { var keep []string for _, m := range f.members[th] { if m != u { keep = append(keep, m) } } f.members[th] = keep return nil } func (f *fakeDiscord) ChannelMessageSendComplex(ch string, data *discordgo.MessageSend, _ ...discordgo.RequestOption) (*discordgo.Message, error) { if f.failSend != nil { return nil, f.failSend } for _, file := range data.Files { raw, _ := io.ReadAll(file.Reader) f.files[file.Name] = string(raw) } f.sent[ch] = append(f.sent[ch], data) m := &discordgo.Message{ID: f.newID(), ChannelID: ch, Content: data.Content, Embeds: data.Embeds, Components: data.Components} f.messages[ch+"/"+m.ID] = m return m, nil } func (f *fakeDiscord) ChannelMessageEditComplex(e *discordgo.MessageEdit, _ ...discordgo.RequestOption) (*discordgo.Message, error) { if f.failEdit != nil { return nil, f.failEdit } f.edits = append(f.edits, e) return &discordgo.Message{ID: e.ID, ChannelID: e.Channel}, nil } func (f *fakeDiscord) ChannelMessage(ch, m string, _ ...discordgo.RequestOption) (*discordgo.Message, error) { if msg, ok := f.messages[ch+"/"+m]; ok { return msg, nil } return nil, errors.New("unknown message") } func (f *fakeDiscord) ChannelMessages(ch string, limit int, before, _, _ string, _ ...discordgo.RequestOption) ([]*discordgo.Message, error) { if f.failFetch != nil { return nil, f.failFetch } // history is stored newest first, like Discord returns it. all := f.history[ch] start := 0 if before != "" { for i, m := range all { if m.ID == before { start = i + 1 } } } end := start + limit if end > len(all) { end = len(all) } return all[start:end], nil } func (f *fakeDiscord) ChannelEditComplex(ch string, data *discordgo.ChannelEdit, _ ...discordgo.RequestOption) (*discordgo.Channel, error) { f.chEdits[ch] = data return &discordgo.Channel{ID: ch}, nil } func (f *fakeDiscord) ChannelDelete(ch string, _ ...discordgo.RequestOption) (*discordgo.Channel, error) { f.deleted = append(f.deleted, ch) return &discordgo.Channel{ID: ch}, nil }