2eme partie last push

This commit is contained in:
lfirmin
2026-05-11 15:47:08 +02:00
parent a5f888b3c1
commit 339ba8fe5b
6 changed files with 179 additions and 64 deletions
+3 -1
View File
@@ -50,6 +50,7 @@ func main() {
ticketRepo := db.NewTicketRepo(sqldb) ticketRepo := db.NewTicketRepo(sqldb)
claimRepo := db.NewClaimMessageRepo(sqldb) claimRepo := db.NewClaimMessageRepo(sqldb)
convocRepo := db.NewConvocationConfigRepo(sqldb) convocRepo := db.NewConvocationConfigRepo(sqldb)
convocPanelRepo := db.NewConvocationPanelRepo(sqldb)
adminRepo := db.NewPanelAdminRepo(sqldb) adminRepo := db.NewPanelAdminRepo(sqldb)
sessionRepo := db.NewPanelSessionRepo(sqldb) sessionRepo := db.NewPanelSessionRepo(sqldb)
auditRepo := db.NewAuditLogRepo(sqldb) auditRepo := db.NewAuditLogRepo(sqldb)
@@ -166,7 +167,7 @@ func main() {
// Start panel HTTP server if configured // Start panel HTTP server if configured
if cfg.Panel.BaseURL != "" || cfg.Panel.Port > 0 { if cfg.Panel.BaseURL != "" || cfg.Panel.Port > 0 {
botSvc := discordbot.NewBotService(bot.Session, panelRepo, ticketRepo, convocRepo, guildID, allowedGuilds) botSvc := discordbot.NewBotService(bot.Session, panelRepo, ticketRepo, convocRepo, convocPanelRepo, guildID, allowedGuilds)
panelSrv := panel.NewServer( panelSrv := panel.NewServer(
cfg.Panel, cfg.Panel,
ticketRepo, ticketRepo,
@@ -175,6 +176,7 @@ func main() {
auditRepo, auditRepo,
panelRepo, panelRepo,
convocRepo, convocRepo,
convocPanelRepo,
guildID, guildID,
botSvc, botSvc,
) )
+79 -14
View File
@@ -15,13 +15,14 @@ import (
// BotServiceImpl implements panel.BotService and handlers.DiscordInfo. // BotServiceImpl implements panel.BotService and handlers.DiscordInfo.
type BotServiceImpl struct { type BotServiceImpl struct {
session *discordgo.Session session *discordgo.Session
panelRepo *db.PanelConfigRepo panelRepo *db.PanelConfigRepo
ticketRepo *db.TicketRepo ticketRepo *db.TicketRepo
convocRepo *db.ConvocationConfigRepo convocRepo *db.ConvocationConfigRepo
guildID string convocPanelRepo *db.ConvocationPanelRepo
allowedGuilds []string guildID string
startedAt time.Time allowedGuilds []string
startedAt time.Time
} }
func NewBotService( func NewBotService(
@@ -29,17 +30,19 @@ func NewBotService(
panelRepo *db.PanelConfigRepo, panelRepo *db.PanelConfigRepo,
ticketRepo *db.TicketRepo, ticketRepo *db.TicketRepo,
convocRepo *db.ConvocationConfigRepo, convocRepo *db.ConvocationConfigRepo,
convocPanelRepo *db.ConvocationPanelRepo,
guildID string, guildID string,
allowedGuilds []string, allowedGuilds []string,
) *BotServiceImpl { ) *BotServiceImpl {
return &BotServiceImpl{ return &BotServiceImpl{
session: s, session: s,
panelRepo: panelRepo, panelRepo: panelRepo,
ticketRepo: ticketRepo, ticketRepo: ticketRepo,
convocRepo: convocRepo, convocRepo: convocRepo,
guildID: guildID, convocPanelRepo: convocPanelRepo,
allowedGuilds: allowedGuilds, guildID: guildID,
startedAt: time.Now(), allowedGuilds: allowedGuilds,
startedAt: time.Now(),
} }
} }
@@ -365,6 +368,68 @@ func (b *BotServiceImpl) SendConvocPanel(guildID string) error {
return b.convocRepo.SetConvocPanelMessage(ctx, guildID, cfg.PanelChannelID, msg.ID) return b.convocRepo.SetConvocPanelMessage(ctx, guildID, cfg.PanelChannelID, msg.ID)
} }
// SendConvocPanelByID sends (or updates) the Discord embed+button for a specific convocation panel.
func (b *BotServiceImpl) SendConvocPanelByID(panelID int64) error {
ctx := context.Background()
p, err := b.convocPanelRepo.GetByID(ctx, panelID)
if err != nil {
return fmt.Errorf("get convoc panel: %w", err)
}
if p == nil {
return fmt.Errorf("convoc panel %d not found", panelID)
}
if p.ChannelID == "" {
return fmt.Errorf("convoc panel %d has no channel configured", panelID)
}
title := p.EmbedTitle
if title == "" {
title = "Créer une convocation"
}
color := parseHexColor(p.EmbedColor)
if color == 0 {
color = 0x5865f2
}
embed := &discordgo.MessageEmbed{
Title: title,
Description: p.EmbedDesc,
Color: color,
}
components := []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{
Label: "Créer une convocation",
Style: discordgo.PrimaryButton,
CustomID: fmt.Sprintf("convoc:panel:%s", p.GuildID),
},
}},
}
if p.MessageID != "" {
_, editErr := b.session.ChannelMessageEditComplex(&discordgo.MessageEdit{
Channel: p.ChannelID,
ID: p.MessageID,
Embeds: &[]*discordgo.MessageEmbed{embed},
Components: &components,
})
if editErr == nil {
return nil
}
// Message deleted — fall through to send new
_ = b.convocPanelRepo.SetMessage(ctx, panelID, "")
}
msg, err := b.session.ChannelMessageSendComplex(p.ChannelID, &discordgo.MessageSend{
Embeds: []*discordgo.MessageEmbed{embed},
Components: components,
})
if err != nil {
return fmt.Errorf("send convoc panel: %w", err)
}
return b.convocPanelRepo.SetMessage(ctx, panelID, msg.ID)
}
func sanitizeConvocUsername(username string) string { func sanitizeConvocUsername(username string) string {
var out []byte var out []byte
for _, b := range []byte(username) { for _, b := range []byte(username) {
+1
View File
@@ -11,4 +11,5 @@ type BotService interface {
DeletePanelMessage(panelID int64) error DeletePanelMessage(panelID int64) error
CreateConvocation(guildID, staffDiscordID, targetDiscordID, reason string) (channelID string, err error) CreateConvocation(guildID, staffDiscordID, targetDiscordID, reason string) (channelID string, err error)
SendConvocPanel(guildID string) error SendConvocPanel(guildID string) error
SendConvocPanelByID(panelID int64) error
} }
+2
View File
@@ -1,9 +1,11 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/go-chi/chi/v5"
"github.com/leolionad58/ticketbot/internal/db" "github.com/leolionad58/ticketbot/internal/db"
panelauth "github.com/leolionad58/ticketbot/internal/panel/auth" panelauth "github.com/leolionad58/ticketbot/internal/panel/auth"
) )
+12 -7
View File
@@ -28,6 +28,7 @@ func NewServer(
auditRepo *db.AuditLogRepo, auditRepo *db.AuditLogRepo,
panelRepo *db.PanelConfigRepo, panelRepo *db.PanelConfigRepo,
convocRepo *db.ConvocationConfigRepo, convocRepo *db.ConvocationConfigRepo,
convocPanelRepo *db.ConvocationPanelRepo,
guildID string, guildID string,
bot BotService, bot BotService,
) *Server { ) *Server {
@@ -90,12 +91,13 @@ func NewServer(
} }
} }
convocH := &handlers.ConvocationsHandler{ convocH := &handlers.ConvocationsHandler{
TicketRepo: sqlDB, TicketRepo: sqlDB,
ConvocRepo: convocRepo, ConvocRepo: convocRepo,
GuildID: guildID, ConvocPanelRepo: convocPanelRepo,
BotService: convocBot, GuildID: guildID,
DiscordInfo: convocInfo, BotService: convocBot,
Renderer: rnd, DiscordInfo: convocInfo,
Renderer: rnd,
} }
transcriptsH := &handlers.TranscriptsHandler{ transcriptsH := &handlers.TranscriptsHandler{
TicketRepo: sqlDB, TicketRepo: sqlDB,
@@ -153,7 +155,10 @@ func NewServer(
// Convocations // Convocations
r.Get("/convocations", convocH.HandleGET) r.Get("/convocations", convocH.HandleGET)
r.Post("/convocations", convocH.HandlePOST) r.Post("/convocations", convocH.HandlePOST)
r.Post("/convocations/send-panel", convocH.HandleSendPanel) r.Post("/convocations/create", convocH.HandleCreate)
r.Post("/convocations/panels/new", convocH.HandleCreatePanel)
r.Post("/convocations/panels/{id}/send", convocH.HandleSendPanel)
r.Post("/convocations/panels/{id}/delete", convocH.HandleDeletePanel)
// Transcripts (list + download only; view is outside securityHeaders) // Transcripts (list + download only; view is outside securityHeaders)
r.Get("/transcripts", transcriptsH.HandleList) r.Get("/transcripts", transcriptsH.HandleList)
+82 -42
View File
@@ -32,58 +32,98 @@
</label> </label>
</div> </div>
<div class="sm:col-span-2 border-t border-discord-border pt-4 mt-1"> <div class="sm:col-span-2 flex justify-end">
<p class="text-xs text-gray-400 uppercase font-semibold tracking-wide mb-3">Panel Discord (bouton convocation)</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs text-gray-400 mb-1">Salon d'envoi du panel</label>
<input type="text" name="panel_channel_id" value="{{if .Config}}{{.Config.PanelChannelID}}{{end}}"
placeholder="ex: 123456789012345678"
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Couleur de l'embed</label>
<input type="color" name="panel_embed_color" value="{{if and .Config .Config.PanelEmbedColor}}{{.Config.PanelEmbedColor}}{{else}}#5865f2{{end}}"
class="h-10 w-full rounded cursor-pointer bg-discord-hover border border-discord-border">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Titre de l'embed</label>
<input type="text" name="panel_embed_title" value="{{if .Config}}{{.Config.PanelEmbedTitle}}{{else}}Créer une convocation{{end}}"
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent"
placeholder="Créer une convocation">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Description de l'embed</label>
<textarea name="panel_embed_description" rows="2"
class="w-full bg-discord-hover border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent resize-none"
placeholder="Cliquez sur le bouton pour ouvrir une convocation...">{{if .Config}}{{.Config.PanelEmbedDescription}}{{end}}</textarea>
</div>
</div>
</div>
<div class="sm:col-span-2 flex items-center gap-3 justify-end">
<button type="submit" <button type="submit"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm font-medium rounded-lg transition-colors"> class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm font-medium rounded-lg transition-colors">
Sauvegarder Sauvegarder
</button> </button>
</div> </div>
</form> </form>
</div>
<!-- Send panel button (separate form) --> <!-- Convocation panels -->
{{if and .Config .Config.PanelChannelID}} <div class="bg-discord-card rounded-lg border border-discord-border p-5 mb-6">
<div class="mt-3 pt-3 border-t border-discord-border flex items-center gap-3"> <h3 class="font-semibold text-white mb-4">Panels de convocation</h3>
<form method="POST" action="/convocations/send-panel">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> {{if .ConvocPanels}}
<button type="submit" <div class="space-y-3 mb-5">
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors"> {{range .ConvocPanels}}
Envoyer le panel sur Discord <div class="flex items-center gap-3 bg-discord-hover rounded-lg px-4 py-3 border border-discord-border">
</button> <div class="w-3 h-3 rounded-full flex-shrink-0" style="background:{{.EmbedColor}}"></div>
</form> <div class="flex-1 min-w-0">
{{if and .Config .Config.PanelMessageID}} <p class="text-sm text-white font-medium truncate">{{.EmbedTitle}}</p>
<span class="text-xs text-green-400 bg-green-900/30 px-2 py-1 rounded">Panel envoyé</span> <p class="text-xs text-gray-400 truncate">Salon : {{if .ChannelID}}{{.ChannelID}}{{else}}<span class="text-red-400">Non configuré</span>{{end}}
{{if .MessageID}}<span class="ml-2 text-green-400">● Envoyé</span>{{end}}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
{{if .ChannelID}}
<form method="POST" action="/convocations/panels/{{.ID}}/send">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button type="submit"
class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium rounded-lg transition-colors">
Envoyer
</button>
</form>
{{end}}
<form method="POST" action="/convocations/panels/{{.ID}}/delete"
onsubmit="return confirm('Supprimer ce panel ?')">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button type="submit"
class="px-3 py-1.5 bg-red-700 hover:bg-red-600 text-white text-xs font-medium rounded-lg transition-colors">
Supprimer
</button>
</form>
</div>
</div>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}
<!-- Create new panel -->
<details class="group">
<summary class="cursor-pointer text-sm text-indigo-400 hover:text-indigo-300 select-none list-none flex items-center gap-1">
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
Ajouter un nouveau panel
</summary>
<form method="POST" action="/convocations/panels/new"
class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div>
<label class="block text-xs text-gray-400 mb-1">Titre de l'embed</label>
<input type="text" name="embed_title" placeholder="Créer une convocation"
class="w-full bg-discord-bg border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
</div>
<div>
<label class="block text-xs text-gray-400 mb-1">Couleur de l'embed</label>
<input type="color" name="embed_color" value="#5865f2"
class="h-10 w-full rounded cursor-pointer bg-discord-hover border border-discord-border">
</div>
<div class="sm:col-span-2">
<label class="block text-xs text-gray-400 mb-1">Description de l'embed</label>
<textarea name="embed_description" rows="2" placeholder="Cliquez sur le bouton pour ouvrir une convocation..."
class="w-full bg-discord-bg border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent resize-none"></textarea>
</div>
<div class="sm:col-span-2">
<label class="block text-xs text-gray-400 mb-1">ID du salon d'envoi</label>
<input type="text" name="channel_id" placeholder="ex: 123456789012345678"
class="w-full bg-discord-bg border border-discord-border rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-discord-accent">
</div>
<div class="sm:col-span-2 flex justify-end">
<button type="submit"
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition-colors">
Créer le panel
</button>
</div>
</form>
</details>
</div> </div>
<!-- Convocation list --> <!-- Convocation list -->