save first version
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package tickets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/leolionad58/ticketbot/internal/config"
|
||||
"github.com/leolionad58/ticketbot/internal/db"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *db.TicketRepo
|
||||
auth *AuthService
|
||||
session *discordgo.Session
|
||||
cfg *config.Provider
|
||||
opening sync.Map // key: userID+":"+ticketType prevents double-click
|
||||
}
|
||||
|
||||
func NewService(repo *db.TicketRepo, auth *AuthService, s *discordgo.Session, cfg *config.Provider) *Service {
|
||||
return &Service{db: repo, auth: auth, session: s, cfg: cfg}
|
||||
}
|
||||
|
||||
var ErrAlreadyOpen = fmt.Errorf("already_open")
|
||||
|
||||
// Open creates a ticket channel and inserts into DB. Returns the created Ticket.
|
||||
// Returns ErrAlreadyOpen (wrapped with channel ID) if user already has an open ticket of the same type.
|
||||
func (svc *Service) Open(ctx context.Context, guildID, userID, panelName, ticketType string) (*db.Ticket, error) {
|
||||
lockKey := userID + ":" + ticketType
|
||||
if _, loaded := svc.opening.LoadOrStore(lockKey, struct{}{}); loaded {
|
||||
return nil, fmt.Errorf("creation already in progress")
|
||||
}
|
||||
defer svc.opening.Delete(lockKey)
|
||||
|
||||
existing, err := svc.db.HasOpenTicket(ctx, userID, ticketType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check existing: %w", err)
|
||||
}
|
||||
if existing != nil {
|
||||
return existing, fmt.Errorf("%w:%s", ErrAlreadyOpen, existing.ChannelID)
|
||||
}
|
||||
|
||||
cfg := svc.cfg.Get()
|
||||
panel, ok := cfg.Panels[panelName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("panel %q not found", panelName)
|
||||
}
|
||||
typeCfg, ok := panel.Types[ticketType]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type %q not found in panel %q", ticketType, panelName)
|
||||
}
|
||||
|
||||
// Reserve a placeholder to get the ticket number atomically
|
||||
ticket := &db.Ticket{
|
||||
UserID: userID,
|
||||
Panel: panelName,
|
||||
Type: ticketType,
|
||||
ChannelID: fmt.Sprintf("pending-%d", time.Now().UnixNano()),
|
||||
OpenedAt: time.Now(),
|
||||
}
|
||||
if err := svc.db.Insert(ctx, ticket); err != nil {
|
||||
return nil, fmt.Errorf("insert ticket: %w", err)
|
||||
}
|
||||
|
||||
channelName := fmt.Sprintf("%s-%04d", ticketType, ticket.TicketNumber)
|
||||
ch, err := svc.session.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
|
||||
Name: channelName,
|
||||
Type: discordgo.ChannelTypeGuildText,
|
||||
ParentID: typeCfg.Category,
|
||||
PermissionOverwrites: []*discordgo.PermissionOverwrite{
|
||||
{
|
||||
ID: guildID,
|
||||
Type: discordgo.PermissionOverwriteTypeRole,
|
||||
Deny: discordgo.PermissionViewChannel,
|
||||
},
|
||||
{
|
||||
ID: userID,
|
||||
Type: discordgo.PermissionOverwriteTypeMember,
|
||||
Allow: discordgo.PermissionViewChannel |
|
||||
discordgo.PermissionSendMessages |
|
||||
discordgo.PermissionReadMessageHistory |
|
||||
discordgo.PermissionAttachFiles,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("create channel", "ticket_id", ticket.ID, "err", err)
|
||||
return nil, fmt.Errorf("create channel: %w", err)
|
||||
}
|
||||
|
||||
if err := svc.db.UpdateChannelID(ctx, ticket.ID, ch.ID); err != nil {
|
||||
slog.Error("update channel_id", "ticket_id", ticket.ID, "err", err)
|
||||
}
|
||||
ticket.ChannelID = ch.ID
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
// AddMember adds a Discord user to a ticket channel with full read/send permissions.
|
||||
func (svc *Service) AddMember(ctx context.Context, channelID, userID string) error {
|
||||
return svc.session.ChannelPermissionSet(channelID, userID,
|
||||
discordgo.PermissionOverwriteTypeMember,
|
||||
discordgo.PermissionViewChannel|discordgo.PermissionSendMessages|
|
||||
discordgo.PermissionReadMessageHistory|discordgo.PermissionAttachFiles,
|
||||
0)
|
||||
}
|
||||
|
||||
// RemoveMember removes a Discord user from a ticket channel.
|
||||
func (svc *Service) RemoveMember(ctx context.Context, channelID, userID string) error {
|
||||
return svc.session.ChannelPermissionDelete(channelID, userID)
|
||||
}
|
||||
|
||||
// Rename sanitizes and renames a ticket channel. Returns error if name is invalid.
|
||||
func (svc *Service) Rename(ctx context.Context, channelID, name string) error {
|
||||
safe := sanitizeChannelName(name)
|
||||
if safe == "" {
|
||||
return fmt.Errorf("invalid channel name after sanitization")
|
||||
}
|
||||
if len(safe) > 100 {
|
||||
safe = safe[:100]
|
||||
}
|
||||
_, err := svc.session.ChannelEdit(channelID, &discordgo.ChannelEdit{Name: safe})
|
||||
return err
|
||||
}
|
||||
|
||||
// ClaimChannel adds a staff member to a ticket channel.
|
||||
func (svc *Service) ClaimChannel(ctx context.Context, channelID, staffID string) error {
|
||||
return svc.AddMember(ctx, channelID, staffID)
|
||||
}
|
||||
|
||||
// DeleteChannel permanently deletes a Discord channel.
|
||||
func (svc *Service) DeleteChannel(channelID string) error {
|
||||
_, err := svc.session.ChannelDelete(channelID)
|
||||
return err
|
||||
}
|
||||
|
||||
func sanitizeChannelName(name string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range strings.ToLower(name) {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-', r == '_':
|
||||
b.WriteRune(r)
|
||||
case unicode.IsSpace(r):
|
||||
b.WriteRune('-')
|
||||
}
|
||||
}
|
||||
return strings.Trim(b.String(), "-")
|
||||
}
|
||||
Reference in New Issue
Block a user