oups
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
}},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user