This commit is contained in:
lfirmin
2026-05-10 18:07:51 +02:00
parent 50af2452b1
commit a5f888b3c1
59 changed files with 6682 additions and 108 deletions
+43 -16
View File
@@ -10,14 +10,15 @@ import (
)
type Bot struct {
Session *discordgo.Session
Config *config.Provider
Tickets *db.TicketRepo
Claims *db.ClaimMessageRepo
GuildID string
Session *discordgo.Session
Config *config.Provider
Tickets *db.TicketRepo
Claims *db.ClaimMessageRepo
GuildID string
AllowedGuilds map[string]bool // set of allowed guild IDs; if empty, all guilds allowed
}
func New(token string, cfg *config.Provider, tickets *db.TicketRepo, claims *db.ClaimMessageRepo, guildID string) (*Bot, error) {
func New(token string, cfg *config.Provider, tickets *db.TicketRepo, claims *db.ClaimMessageRepo, guildID string, allowedGuilds []string) (*Bot, error) {
s, err := discordgo.New("Bot " + token)
if err != nil {
return nil, fmt.Errorf("create session: %w", err)
@@ -27,15 +28,29 @@ func New(token string, cfg *config.Provider, tickets *db.TicketRepo, claims *db.
discordgo.IntentsGuildMessages |
discordgo.IntentsMessageContent
allowed := make(map[string]bool, len(allowedGuilds))
for _, id := range allowedGuilds {
if id != "" {
allowed[id] = true
}
}
return &Bot{
Session: s,
Config: cfg,
Tickets: tickets,
Claims: claims,
GuildID: guildID,
Session: s,
Config: cfg,
Tickets: tickets,
Claims: claims,
GuildID: guildID,
AllowedGuilds: allowed,
}, nil
}
func (b *Bot) IsGuildAllowed(guildID string) bool {
if len(b.AllowedGuilds) == 0 {
return true
}
return b.AllowedGuilds[guildID]
}
func (b *Bot) Open() error {
return b.Session.Open()
}
@@ -46,12 +61,24 @@ func (b *Bot) Close() {
func (b *Bot) RegisterCommands(appID string) error {
cmds := ApplicationCommands()
for _, cmd := range cmds {
_, err := b.Session.ApplicationCommandCreate(appID, b.GuildID, cmd)
if err != nil {
return fmt.Errorf("register %s: %w", cmd.Name, err)
for guildID := range b.AllowedGuilds {
for _, cmd := range cmds {
_, err := b.Session.ApplicationCommandCreate(appID, guildID, cmd)
if err != nil {
return fmt.Errorf("register %s in guild %s: %w", cmd.Name, guildID, err)
}
slog.Info("registered command", "name", cmd.Name, "guild_id", guildID)
}
}
// If no allowed guilds configured, register globally (or in default guild)
if len(b.AllowedGuilds) == 0 {
for _, cmd := range cmds {
_, err := b.Session.ApplicationCommandCreate(appID, b.GuildID, cmd)
if err != nil {
return fmt.Errorf("register %s: %w", cmd.Name, err)
}
slog.Info("registered command", "name", cmd.Name)
}
slog.Info("registered command", "name", cmd.Name)
}
return nil
}
+403
View File
@@ -0,0 +1,403 @@
package discord
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/bwmarrin/discordgo"
"github.com/leolionad58/ticketbot/internal/db"
"github.com/leolionad58/ticketbot/internal/panel/handlers"
)
// BotServiceImpl implements panel.BotService and handlers.DiscordInfo.
type BotServiceImpl struct {
session *discordgo.Session
panelRepo *db.PanelConfigRepo
ticketRepo *db.TicketRepo
convocRepo *db.ConvocationConfigRepo
guildID string
allowedGuilds []string
startedAt time.Time
}
func NewBotService(
s *discordgo.Session,
panelRepo *db.PanelConfigRepo,
ticketRepo *db.TicketRepo,
convocRepo *db.ConvocationConfigRepo,
guildID string,
allowedGuilds []string,
) *BotServiceImpl {
return &BotServiceImpl{
session: s,
panelRepo: panelRepo,
ticketRepo: ticketRepo,
convocRepo: convocRepo,
guildID: guildID,
allowedGuilds: allowedGuilds,
startedAt: time.Now(),
}
}
// GetBotStatus returns live bot metrics.
func (b *BotServiceImpl) GetBotStatus() handlers.BotStatus {
guildName := ""
if g, err := b.session.State.Guild(b.guildID); err == nil && g != nil {
guildName = g.Name
}
return handlers.BotStatus{
Online: true,
StartedAt: b.startedAt,
LatencyMs: b.session.HeartbeatLatency().Milliseconds(),
GuildName: guildName,
}
}
// GetGuildList returns all whitelisted guilds by name.
func (b *BotServiceImpl) GetGuildList() []handlers.GuildInfo {
seen := map[string]bool{}
ids := append([]string{}, b.allowedGuilds...)
if b.guildID != "" && !seen[b.guildID] {
ids = append(ids, b.guildID)
}
out := make([]handlers.GuildInfo, 0, len(ids))
for _, id := range ids {
if seen[id] || id == "" {
continue
}
seen[id] = true
name := id
if g, err := b.session.State.Guild(id); err == nil && g != nil {
name = g.Name
}
out = append(out, handlers.GuildInfo{ID: id, Name: name})
}
return out
}
// SendPanel sends or updates the Discord panel message for the given panel ID.
func (b *BotServiceImpl) SendPanel(panelID int64) error {
ctx := context.Background()
p, err := b.panelRepo.GetByID(ctx, panelID)
if err != nil {
return fmt.Errorf("get panel: %w", err)
}
if p == nil {
return fmt.Errorf("panel %d not found", panelID)
}
if !p.ChannelID.Valid || p.ChannelID.String == "" {
return fmt.Errorf("panel %d has no channel configured", panelID)
}
embed := &discordgo.MessageEmbed{
Title: p.EmbedTitle,
Description: p.EmbedDescription.String,
Color: parseHexColor(p.EmbedColor),
}
if p.EmbedThumbnail != "" {
embed.Thumbnail = &discordgo.MessageEmbedThumbnail{URL: p.EmbedThumbnail}
}
if p.EmbedImage != "" {
embed.Image = &discordgo.MessageEmbedImage{URL: p.EmbedImage}
}
// Build action rows (max 5 buttons per row)
var components []discordgo.MessageComponent
var row []discordgo.MessageComponent
for i, t := range p.Types {
btn := discordgo.Button{
Label: t.ButtonLabel,
Style: buttonStyle(t.ButtonColor),
CustomID: fmt.Sprintf("panel:open:%s:%s", p.Name, t.Name),
}
if t.ButtonEmoji != "" {
emoji := discordgo.ComponentEmoji{Name: t.ButtonEmoji}
btn.Emoji = &emoji
}
row = append(row, btn)
if len(row) == 5 || i == len(p.Types)-1 {
components = append(components, discordgo.ActionsRow{Components: row})
row = nil
}
}
channelID := p.ChannelID.String
if p.MessageID.Valid && p.MessageID.String != "" {
_, editErr := b.session.ChannelMessageEditComplex(&discordgo.MessageEdit{
Channel: channelID,
ID: p.MessageID.String,
Embeds: &[]*discordgo.MessageEmbed{embed},
Components: &components,
})
if editErr == nil {
return nil
}
// Message no longer exists — clear the stored ID and fall through to send new.
_ = b.panelRepo.SetDiscordMessage(ctx, panelID, channelID, "")
}
msg, err := b.session.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{embed},
Components: components,
})
if err != nil {
return fmt.Errorf("send panel message: %w", err)
}
return b.panelRepo.SetDiscordMessage(ctx, panelID, channelID, msg.ID)
}
// DeletePanelMessage deletes the Discord panel message.
func (b *BotServiceImpl) DeletePanelMessage(panelID int64) error {
ctx := context.Background()
p, err := b.panelRepo.GetByID(ctx, panelID)
if err != nil || p == nil {
return nil
}
if p.MessageID.Valid && p.ChannelID.Valid && p.MessageID.String != "" {
_ = b.session.ChannelMessageDelete(p.ChannelID.String, p.MessageID.String)
_ = b.panelRepo.SetDiscordMessage(ctx, panelID, "", "")
}
return nil
}
// GetGuildRoles returns all roles for the given guild.
func (b *BotServiceImpl) GetGuildRoles(guildID string) ([]handlers.GuildRole, error) {
roles, err := b.session.GuildRoles(guildID)
if err != nil {
return nil, err
}
out := make([]handlers.GuildRole, 0, len(roles))
for _, r := range roles {
if r.Name == "@everyone" {
continue
}
color := "#99aab5"
if r.Color != 0 {
color = fmt.Sprintf("#%06x", r.Color)
}
out = append(out, handlers.GuildRole{ID: r.ID, Name: r.Name, Color: color})
}
return out, nil
}
// GetGuildChannels returns all channels for the given guild.
func (b *BotServiceImpl) GetGuildChannels(guildID string) ([]handlers.GuildChannel, error) {
channels, err := b.session.GuildChannels(guildID)
if err != nil {
return nil, err
}
out := make([]handlers.GuildChannel, 0, len(channels))
for _, c := range channels {
out = append(out, handlers.GuildChannel{
ID: c.ID,
Name: c.Name,
Type: int(c.Type),
})
}
return out, nil
}
// CreateConvocation creates a convocation channel on Discord and records it in the DB.
// staffDiscordID is the Discord ID of the admin creating the convocation.
func (b *BotServiceImpl) CreateConvocation(guildID, staffDiscordID, targetDiscordID, reason string) (string, error) {
ctx := context.Background()
// Get convocation category from DB config
cfg, err := b.convocRepo.Get(ctx, guildID)
if err != nil {
return "", fmt.Errorf("get convoc config: %w", err)
}
categoryID := ""
if cfg != nil {
categoryID = cfg.CategoryID
}
if categoryID == "" {
return "", fmt.Errorf("aucune catégorie de convocation configurée pour cette guild")
}
// Get target member info
target, err := b.session.GuildMember(guildID, targetDiscordID)
if err != nil {
return "", fmt.Errorf("utilisateur introuvable dans la guild : %w", err)
}
n, err := b.ticketRepo.NextConvocationNumber(ctx)
if err != nil {
return "", fmt.Errorf("next convocation number: %w", err)
}
username := target.User.Username
channelName := fmt.Sprintf("convoc-%s-%04d", sanitizeConvocUsername(username), n)
if len(channelName) > 100 {
channelName = channelName[:100]
}
ch, err := b.session.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
Name: channelName,
Type: discordgo.ChannelTypeGuildText,
ParentID: categoryID,
PermissionOverwrites: []*discordgo.PermissionOverwrite{
{
ID: guildID,
Type: discordgo.PermissionOverwriteTypeRole,
Deny: discordgo.PermissionViewChannel,
},
{
ID: staffDiscordID,
Type: discordgo.PermissionOverwriteTypeMember,
Allow: discordgo.PermissionViewChannel |
discordgo.PermissionSendMessages |
discordgo.PermissionReadMessageHistory |
discordgo.PermissionAttachFiles,
},
{
ID: targetDiscordID,
Type: discordgo.PermissionOverwriteTypeMember,
Allow: discordgo.PermissionViewChannel |
discordgo.PermissionSendMessages |
discordgo.PermissionReadMessageHistory,
},
},
})
if err != nil {
return "", fmt.Errorf("create channel: %w", err)
}
deleteCustomID := fmt.Sprintf("convoc:delete:%s:%s", ch.ID, staffDiscordID)
if len(deleteCustomID) > 100 {
deleteCustomID = "convoc:delete:" + ch.ID
}
b.session.ChannelMessageSendComplex(ch.ID, &discordgo.MessageSend{ //nolint
Content: fmt.Sprintf("<@%s>", targetDiscordID),
Embeds: []*discordgo.MessageEmbed{
{
Title: "Convocation",
Description: fmt.Sprintf("Tu as été convoqué par <@%s>.\n\n**Raison :** %s", staffDiscordID, reason),
Color: 0x5865f2,
},
},
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Supprimer",
Style: discordgo.DangerButton,
CustomID: deleteCustomID,
},
}},
},
})
ticket := &db.Ticket{
UserID: targetDiscordID,
Panel: "convocation",
Type: "convocation",
ChannelID: ch.ID,
OpenedAt: time.Now(),
Status: "open",
TicketNumber: n,
}
_ = b.ticketRepo.InsertWithNumber(ctx, ticket)
return ch.ID, nil
}
// SendConvocPanel sends (or updates) the convocation panel embed+button in the configured Discord channel.
func (b *BotServiceImpl) SendConvocPanel(guildID string) error {
ctx := context.Background()
cfg, err := b.convocRepo.Get(ctx, guildID)
if err != nil {
return fmt.Errorf("get convoc config: %w", err)
}
if cfg == nil || cfg.PanelChannelID == "" {
return fmt.Errorf("aucun salon de panel convocation configuré")
}
title := cfg.PanelEmbedTitle
if title == "" {
title = "Créer une convocation"
}
color := parseHexColor(cfg.PanelEmbedColor)
if color == 0 {
color = 0x5865f2
}
embed := &discordgo.MessageEmbed{
Title: title,
Description: cfg.PanelEmbedDescription,
Color: color,
}
components := []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Créer une convocation",
Style: discordgo.PrimaryButton,
CustomID: "convoc:panel:" + guildID,
},
}},
}
if cfg.PanelMessageID != "" {
_, editErr := b.session.ChannelMessageEditComplex(&discordgo.MessageEdit{
Channel: cfg.PanelChannelID,
ID: cfg.PanelMessageID,
Embeds: &[]*discordgo.MessageEmbed{embed},
Components: &components,
})
if editErr == nil {
return nil
}
// Message deleted — fall through to send new
_ = b.convocRepo.SetConvocPanelMessage(ctx, guildID, cfg.PanelChannelID, "")
}
msg, err := b.session.ChannelMessageSendComplex(cfg.PanelChannelID, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{embed},
Components: components,
})
if err != nil {
return fmt.Errorf("send convoc panel: %w", err)
}
return b.convocRepo.SetConvocPanelMessage(ctx, guildID, cfg.PanelChannelID, msg.ID)
}
func sanitizeConvocUsername(username string) string {
var out []byte
for _, b := range []byte(username) {
if (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '-' {
out = append(out, b)
} else if b >= 'A' && b <= 'Z' {
out = append(out, b+32)
}
}
if len(out) == 0 {
return "user"
}
if len(out) > 20 {
out = out[:20]
}
return string(out)
}
func parseHexColor(s string) int {
s = strings.TrimPrefix(s, "#")
n, _ := strconv.ParseInt(s, 16, 64)
return int(n)
}
func buttonStyle(color string) discordgo.ButtonStyle {
switch color {
case "secondary":
return discordgo.SecondaryButton
case "success":
return discordgo.SuccessButton
case "danger":
return discordgo.DangerButton
default:
return discordgo.PrimaryButton
}
}
+253
View File
@@ -0,0 +1,253 @@
package components
import (
"context"
"fmt"
"log/slog"
"strings"
"time"
"github.com/bwmarrin/discordgo"
"github.com/leolionad58/ticketbot/internal/db"
"github.com/leolionad58/ticketbot/internal/logger"
"github.com/leolionad58/ticketbot/internal/tickets"
)
// ConvocationComponent handles the convocation panel button and modal.
type ConvocationComponent struct {
ConvocRepo *db.ConvocationConfigRepo
TicketRepo *db.TicketRepo
Auth *tickets.AuthService
LogSvc *logger.DiscordLogger
}
// HandlePanel handles clicks on convoc:panel:<guildID> buttons.
// Shows a modal asking for target user ID and reason.
func (c *ConvocationComponent) HandlePanel(s *discordgo.Session, i *discordgo.InteractionCreate) {
staffID := interactionUserID(i)
memberRoles := memberRoleIDs(i)
if !c.Auth.Can(memberRoles, tickets.ActionConvocation, nil) {
ephemeral(s, i, "Tu n'as pas la permission d'effectuer cette action.")
slog.Warn("convoc panel: denied", "user_id", staffID)
return
}
guildID := interactionGuildID(i)
parts := strings.SplitN(i.MessageComponentData().CustomID, ":", 3)
if len(parts) == 3 && parts[2] != "" {
guildID = parts[2]
}
modalID := "modal:convoc:" + guildID
if len(modalID) > 100 {
modalID = modalID[:100]
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ //nolint
Type: discordgo.InteractionResponseModal,
Data: &discordgo.InteractionResponseData{
CustomID: modalID,
Title: "Créer une convocation",
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.TextInput{
CustomID: "target",
Label: "ID Discord de l'utilisateur",
Style: discordgo.TextInputShort,
Required: true,
MaxLength: 50,
Placeholder: "123456789012345678",
},
}},
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.TextInput{
CustomID: "reason",
Label: "Raison de la convocation",
Style: discordgo.TextInputParagraph,
Required: true,
MaxLength: 500,
Placeholder: "Motif de la convocation...",
},
}},
},
},
})
}
// HandleModalSubmit handles modal:convoc:<guildID> submissions.
func (c *ConvocationComponent) HandleModalSubmit(s *discordgo.Session, i *discordgo.InteractionCreate) {
parts := strings.SplitN(i.ModalSubmitData().CustomID, ":", 3)
if len(parts) != 3 {
return
}
guildID := parts[2]
if guildID == "" {
guildID = interactionGuildID(i)
}
var targetID, reason string
for _, row := range i.ModalSubmitData().Components {
if ar, ok := row.(*discordgo.ActionsRow); ok {
for _, comp := range ar.Components {
if ti, ok := comp.(*discordgo.TextInput); ok {
switch ti.CustomID {
case "target":
targetID = strings.TrimSpace(ti.Value)
case "reason":
reason = strings.TrimSpace(ti.Value)
}
}
}
}
}
if targetID == "" || reason == "" {
ephemeral(s, i, "L'ID utilisateur et la raison sont requis.")
return
}
// Strip mention format <@ID> or <@!ID>
targetID = strings.TrimPrefix(targetID, "<@!")
targetID = strings.TrimPrefix(targetID, "<@")
targetID = strings.TrimSuffix(targetID, ">")
targetID = strings.TrimSpace(targetID)
staffID := interactionUserID(i)
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ //nolint
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{Flags: discordgo.MessageFlagsEphemeral},
})
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cfg, err := c.ConvocRepo.Get(ctx, guildID)
if err != nil || cfg == nil || cfg.CategoryID == "" {
followup(s, i, "La catégorie de convocation n'est pas configurée.")
return
}
target, err := s.GuildMember(guildID, targetID)
if err != nil {
followup(s, i, fmt.Sprintf("Utilisateur introuvable dans ce serveur : %s", targetID))
return
}
if target.User.Bot {
followup(s, i, "Tu ne peux pas convoquer un bot.")
return
}
if target.User.ID == staffID {
followup(s, i, "Tu ne peux pas te convoquer toi-même.")
return
}
n, err := c.TicketRepo.NextConvocationNumber(ctx)
if err != nil {
slog.Error("convoc modal: next number", "err", err)
followup(s, i, "Erreur interne.")
return
}
username := target.User.Username
channelName := fmt.Sprintf("convoc-%s-%04d", sanitizeConvocN(username), n)
if len(channelName) > 100 {
channelName = channelName[:100]
}
ch, err := s.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{
Name: channelName,
Type: discordgo.ChannelTypeGuildText,
ParentID: cfg.CategoryID,
PermissionOverwrites: []*discordgo.PermissionOverwrite{
{
ID: guildID,
Type: discordgo.PermissionOverwriteTypeRole,
Deny: discordgo.PermissionViewChannel,
},
{
ID: staffID,
Type: discordgo.PermissionOverwriteTypeMember,
Allow: discordgo.PermissionViewChannel |
discordgo.PermissionSendMessages |
discordgo.PermissionReadMessageHistory |
discordgo.PermissionAttachFiles,
},
{
ID: target.User.ID,
Type: discordgo.PermissionOverwriteTypeMember,
Allow: discordgo.PermissionViewChannel |
discordgo.PermissionSendMessages |
discordgo.PermissionReadMessageHistory,
},
},
})
if err != nil {
slog.Error("convoc modal: create channel", "err", err)
followup(s, i, "Erreur lors de la création du salon.")
return
}
deleteCustomID := fmt.Sprintf("convoc:delete:%s:%s", ch.ID, staffID)
if len(deleteCustomID) > 100 {
deleteCustomID = "convoc:delete:" + ch.ID
}
s.ChannelMessageSendComplex(ch.ID, &discordgo.MessageSend{ //nolint
Content: fmt.Sprintf("<@%s>", target.User.ID),
Embeds: []*discordgo.MessageEmbed{
{
Title: "Convocation",
Description: fmt.Sprintf("Tu as été convoqué par <@%s>.\n\n**Raison :** %s", staffID, reason),
Color: 0x5865f2,
},
},
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Supprimer",
Style: discordgo.DangerButton,
CustomID: deleteCustomID,
},
}},
},
})
ticket := &db.Ticket{
UserID: target.User.ID,
Panel: "convocation",
Type: "convocation",
ChannelID: ch.ID,
GuildID: guildID,
LogChannelID: cfg.LogChannelID,
OpenedAt: time.Now(),
Status: "open",
TicketNumber: n,
}
if err := c.TicketRepo.InsertWithNumber(ctx, ticket); err != nil {
slog.Error("convoc modal: insert DB", "err", err)
}
c.LogSvc.LogConvocationOpened(staffID, target.User.ID, reason, ch.ID)
slog.Info("convocation created via panel", "channel_id", ch.ID, "staff_id", staffID, "target_id", target.User.ID)
followup(s, i, fmt.Sprintf("Convocation créée : <#%s>", ch.ID))
}
func sanitizeConvocN(username string) string {
var out []byte
for _, b := range []byte(username) {
if (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '-' {
out = append(out, b)
} else if b >= 'A' && b <= 'Z' {
out = append(out, b+32)
}
}
if len(out) == 0 {
return "user"
}
if len(out) > 20 {
out = out[:20]
}
return string(out)
}
+48 -9
View File
@@ -19,6 +19,7 @@ import (
type PanelComponent struct {
TicketSvc *tickets.Service
TicketRepo *db.TicketRepo
PanelRepo *db.PanelConfigRepo
ClaimMgr *claim.Manager
LogSvc *logger.DiscordLogger
Config *config.Provider
@@ -123,26 +124,48 @@ func (c *PanelComponent) HandleModalSubmit(s *discordgo.Session, i *discordgo.In
ticket.TicketTitle = db.NullStr(ticketTitle)
ticket.TicketDescription = db.NullStr(ticketDescription)
cfg := c.Config.Get()
var embedTitle, embedText, embedColor string
if panel, ok := cfg.Panels[panelName]; ok {
var embedTitle, embedText, embedColor, thumbnailURL, imageURL string
yamlCfg := c.Config.Get()
if panel, ok := yamlCfg.Panels[panelName]; ok {
if typeCfg, ok := panel.Types[ticketType]; ok {
embedTitle = typeCfg.EmbedTitle
embedText = typeCfg.EmbedText
embedColor = typeCfg.EmbedColor
}
} else if c.PanelRepo != nil {
if dbPanel, err := c.PanelRepo.GetByName(context.Background(), panelName); err == nil && dbPanel != nil {
for _, t := range dbPanel.Types {
if t.Name == ticketType {
embedTitle = t.EmbedTitle
embedText = t.EmbedText
embedColor = t.EmbedColor
thumbnailURL = t.ThumbnailURL
imageURL = t.ImageURL
break
}
}
}
}
// a) Welcome embed (bot identity, no webhook)
welcomeEmbed := &discordgo.MessageEmbed{
Title: embedTitle,
Description: embedText,
Color: parseColor(embedColor),
}
if thumbnailURL != "" {
welcomeEmbed.Thumbnail = &discordgo.MessageEmbedThumbnail{URL: thumbnailURL}
}
if imageURL != "" {
welcomeEmbed.Image = &discordgo.MessageEmbedImage{URL: imageURL}
}
deleteCustomID := "ticket:delete:confirm:" + ticket.ChannelID
if _, err := s.ChannelMessageSendComplex(ticket.ChannelID, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{
{Title: embedTitle, Description: embedText, Color: parseColor(embedColor)},
},
Embeds: []*discordgo.MessageEmbed{welcomeEmbed},
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Supprimer le ticket",
Label: "Fermer le ticket",
Style: discordgo.DangerButton,
CustomID: deleteCustomID,
},
@@ -155,8 +178,21 @@ func (c *PanelComponent) HandleModalSubmit(s *discordgo.Session, i *discordgo.In
// b) Webhook impersonation message
postImpersonationMessage(s, ticket, uID, ticketTitle, ticketDescription, i.GuildID)
// c) Claim channel message (includes title + description)
c.ClaimMgr.StartTicket(ctx, s, ticket)
// c) Claim channel message — only if claim mode is enabled for this panel type
claimEnabled := true
if c.PanelRepo != nil {
if dbPanel, err2 := c.PanelRepo.GetByName(context.Background(), panelName); err2 == nil && dbPanel != nil {
for _, t := range dbPanel.Types {
if t.Name == ticketType {
claimEnabled = t.ClaimMode != 0
break
}
}
}
}
if claimEnabled {
c.ClaimMgr.StartTicket(ctx, s, ticket)
}
c.LogSvc.LogTicketOpened(ticket, uID)
slog.Info("ticket opened via modal", "ticket_id", ticket.ID, "channel_id", ticket.ChannelID, "user_id", uID)
@@ -175,6 +211,9 @@ func postImpersonationMessage(s *discordgo.Session, ticket *db.Ticket, userID, t
user := member.User
avatarURL := user.AvatarURL("128")
displayName := user.Username
if user.GlobalName != "" {
displayName = user.GlobalName
}
if member.Nick != "" {
displayName = member.Nick
}
+2 -2
View File
@@ -60,11 +60,11 @@ func (c *TicketComponent) HandleDeleteConfirm(s *discordgo.Session, i *discordgo
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ //nolint
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Es-tu sûr de vouloir supprimer ce ticket ? Cette action est irréversible.",
Content: "Es-tu sûr de vouloir fermer ce ticket ?",
Flags: discordgo.MessageFlagsEphemeral,
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{Label: "Oui, supprimer", Style: discordgo.DangerButton, CustomID: yesID},
discordgo.Button{Label: "Oui, fermer", Style: discordgo.DangerButton, CustomID: yesID},
discordgo.Button{Label: "Annuler", Style: discordgo.SecondaryButton, CustomID: cancelID},
}},
},
+19 -5
View File
@@ -11,14 +11,20 @@ import (
// Router dispatches Discord interactions to the appropriate handler.
type Router struct {
PanelCmd *commands.PanelCommand
TicketCmd *commands.TicketCommand
ConvocCmd *commands.ConvocationCommand
PanelComp *components.PanelComponent
TicketComp *components.TicketComponent
PanelCmd *commands.PanelCommand
TicketCmd *commands.TicketCommand
ConvocCmd *commands.ConvocationCommand
PanelComp *components.PanelComponent
TicketComp *components.TicketComponent
ConvocComp *components.ConvocationComponent
IsGuildAllowed func(guildID string) bool
}
func (r *Router) Handle(s *discordgo.Session, i *discordgo.InteractionCreate) {
if r.IsGuildAllowed != nil && !r.IsGuildAllowed(i.GuildID) {
slog.Debug("ignoring interaction from unlisted guild", "guild_id", i.GuildID)
return
}
switch i.Type {
case discordgo.InteractionApplicationCommand:
r.routeCommand(s, i)
@@ -61,6 +67,10 @@ func (r *Router) routeComponent(s *discordgo.Session, i *discordgo.InteractionCr
r.TicketComp.HandleDeleteCancel(s, i)
case strings.HasPrefix(customID, "convoc:delete:"):
r.TicketComp.HandleConvocationDelete(s, i)
case strings.HasPrefix(customID, "convoc:panel:"):
if r.ConvocComp != nil {
r.ConvocComp.HandlePanel(s, i)
}
default:
slog.Warn("unknown component", "custom_id", customID)
}
@@ -72,6 +82,10 @@ func (r *Router) routeModal(s *discordgo.Session, i *discordgo.InteractionCreate
switch {
case strings.HasPrefix(customID, "modal:ticket:"):
r.PanelComp.HandleModalSubmit(s, i)
case strings.HasPrefix(customID, "modal:convoc:"):
if r.ConvocComp != nil {
r.ConvocComp.HandleModalSubmit(s, i)
}
default:
slog.Warn("unknown modal", "custom_id", customID)
}