191 lines
5.4 KiB
Go
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
|
|
}
|