Files
bot_ticket_r6_elite/internal/claim/manager.go
T
2026-05-02 02:55:53 +02:00

191 lines
5.4 KiB
Go

package claim
import (
"context"
"fmt"
"log/slog"
"sync"
"time"
"github.com/bwmarrin/discordgo"
"github.com/leolionad58/ticketbot/internal/config"
"github.com/leolionad58/ticketbot/internal/db"
)
type Manager struct {
cfg *config.Provider
repo *db.TicketRepo
claims *db.ClaimMessageRepo
mu sync.Mutex
stops map[int64]context.CancelFunc
}
func NewManager(cfg *config.Provider, repo *db.TicketRepo, claims *db.ClaimMessageRepo) *Manager {
return &Manager{
cfg: cfg,
repo: repo,
claims: claims,
stops: make(map[int64]context.CancelFunc),
}
}
// StartTicket posts the initial claim message and starts the re-up goroutine.
func (m *Manager) StartTicket(ctx context.Context, s *discordgo.Session, ticket *db.Ticket) {
msgID, err := m.postClaim(ctx, s, ticket, false)
if err != nil {
slog.Error("claim: post initial message", "ticket_id", ticket.ID, "err", err)
} else {
if err := m.claims.Upsert(ctx, ticket.ID, msgID, time.Now()); err != nil {
slog.Error("claim: upsert claim_message", "ticket_id", ticket.ID, "err", err)
}
}
m.startReupLoop(s, ticket, time.Now())
}
// ResumeTicket re-attaches the re-up loop after a bot restart.
func (m *Manager) ResumeTicket(s *discordgo.Session, ticket *db.Ticket, lastReupAt time.Time) {
m.startReupLoop(s, ticket, lastReupAt)
}
// Stop cancels the re-up loop for a ticket (call on claim or close).
func (m *Manager) Stop(ticketID int64) {
m.mu.Lock()
defer m.mu.Unlock()
if cancel, ok := m.stops[ticketID]; ok {
cancel()
delete(m.stops, ticketID)
}
}
// DeleteClaimMessage removes the claim message from the claim channel.
func (m *Manager) DeleteClaimMessage(ctx context.Context, s *discordgo.Session, ticketID int64) {
cm, err := m.claims.Get(ctx, ticketID)
if err != nil || cm == nil {
return
}
cfg := m.cfg.Get()
if err := s.ChannelMessageDelete(cfg.Bot.ClaimChannel, cm.MessageID); err != nil {
slog.Warn("claim: delete message", "ticket_id", ticketID, "err", err)
}
m.claims.Delete(ctx, ticketID) //nolint
}
func (m *Manager) startReupLoop(s *discordgo.Session, ticket *db.Ticket, lastReupAt time.Time) {
ctx, cancel := context.WithCancel(context.Background())
m.mu.Lock()
// Cancel previous loop if any (shouldn't happen in normal flow)
if old, ok := m.stops[ticket.ID]; ok {
old()
}
m.stops[ticket.ID] = cancel
m.mu.Unlock()
go func() {
defer func() {
m.mu.Lock()
delete(m.stops, ticket.ID)
m.mu.Unlock()
}()
cfg := m.cfg.Get()
interval := time.Duration(cfg.Bot.ClaimReupMinutes) * time.Minute
elapsed := time.Since(lastReupAt)
next := interval - elapsed
if next <= 0 {
next = time.Second // fire almost immediately if already overdue
}
timer := time.NewTimer(next)
defer timer.Stop()
reupCount := 0
for {
select {
case <-ctx.Done():
return
case <-timer.C:
// Verify ticket is still open before re-posting
checkCtx, checkCancel := context.WithTimeout(ctx, 5*time.Second)
t, err := m.repo.GetByID(checkCtx, ticket.ID)
checkCancel()
if err != nil || t == nil || t.Status != "open" {
return
}
// Delete old claim message
delCtx, delCancel := context.WithTimeout(ctx, 5*time.Second)
m.DeleteClaimMessage(delCtx, s, ticket.ID)
delCancel()
// Re-post with staff ping (first re-up and beyond)
postCtx, postCancel := context.WithTimeout(ctx, 10*time.Second)
msgID, postErr := m.postClaim(postCtx, s, ticket, reupCount >= 0)
postCancel()
if postErr != nil {
slog.Error("claim reup: post", "ticket_id", ticket.ID, "err", postErr)
} else {
saveCtx, saveCancel := context.WithTimeout(context.Background(), 3*time.Second)
m.claims.Upsert(saveCtx, ticket.ID, msgID, time.Now()) //nolint
saveCancel()
}
reupCount++
// Re-read config in case of hot reload
cfg = m.cfg.Get()
interval = time.Duration(cfg.Bot.ClaimReupMinutes) * time.Minute
timer.Reset(interval)
}
}
}()
}
func (m *Manager) postClaim(ctx context.Context, s *discordgo.Session, ticket *db.Ticket, withPing bool) (string, error) {
cfg := m.cfg.Get()
claimChannelID := cfg.Bot.ClaimChannel
if claimChannelID == "" {
return "", fmt.Errorf("claim_channel not configured")
}
elapsed := time.Since(ticket.OpenedAt).Round(time.Second)
embed := &discordgo.MessageEmbed{
Title: "Nouveau ticket à claim",
Color: 0x5865f2,
Fields: []*discordgo.MessageEmbedField{
{Name: "Type", Value: ticket.Type, Inline: true},
{Name: "Panel", Value: ticket.Panel, Inline: true},
{Name: "Utilisateur", Value: fmt.Sprintf("<@%s>", ticket.UserID), Inline: true},
{Name: "Numéro", Value: fmt.Sprintf("%04d", ticket.TicketNumber), Inline: true},
{Name: "Channel", Value: fmt.Sprintf("<#%s>", ticket.ChannelID), Inline: true},
{Name: "Ouvert il y a", Value: elapsed.String(), Inline: true},
},
}
customID := fmt.Sprintf("claim:take:%d", ticket.ID)
send := &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{embed},
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Claim",
Style: discordgo.SuccessButton,
CustomID: customID,
},
}},
},
}
if withPing {
if panel, ok := cfg.Panels[ticket.Panel]; ok {
if t, ok := panel.Types[ticket.Type]; ok && t.StaffRole != "" {
send.Content = fmt.Sprintf("<@&%s>", t.StaffRole)
}
}
}
msg, err := s.ChannelMessageSendComplex(claimChannelID, send)
if err != nil {
return "", err
}
return msg.ID, nil
}