package handlers import ( "fmt" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/leolionad58/ticketbot/internal/db" panelauth "github.com/leolionad58/ticketbot/internal/panel/auth" ) // ConvocBotService is the subset of BotService needed for convocation operations. type ConvocBotService interface { CreateConvocation(guildID, staffDiscordID, targetDiscordID, reason string) (channelID string, err error) SendConvocPanel(guildID string) error SendConvocPanelByID(panelID int64) error } // ConvocDiscordInfo is the subset of DiscordInfo for guild selection in convocations. type ConvocDiscordInfo interface { GetGuildList() []GuildInfo } // ConvocationsHandler serves /convocations. type ConvocationsHandler struct { TicketRepo *db.TicketRepo ConvocRepo *db.ConvocationConfigRepo ConvocPanelRepo *db.ConvocationPanelRepo GuildID string BotService ConvocBotService DiscordInfo ConvocDiscordInfo Renderer *Renderer } type convocationsData struct { baseData Config *db.ConvocationConfig ConvocPanels []*db.ConvocationPanel Tickets []*db.Ticket Total int Page int Pages int Guilds []GuildInfo } // HandleGET serves GET /convocations. func (h *ConvocationsHandler) HandleGET(w http.ResponseWriter, r *http.Request) { ctx := r.Context() base := h.Renderer.base(r, "convocations") sess := panelauth.SessionFromContext(ctx) if sess != nil { base.CSRFToken = sess.CSRFToken } cfg, err := h.ConvocRepo.Get(ctx, h.GuildID) if err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) return } var convocPanels []*db.ConvocationPanel if h.ConvocPanelRepo != nil { convocPanels, _ = h.ConvocPanelRepo.List(ctx, h.GuildID) } page := 1 tickets, total, err := h.TicketRepo.ListFiltered(ctx, db.TicketFilter{ Type: "convocation", Page: page, PageSize: 20, }) if err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) return } pages := total / 20 if total%20 != 0 { pages++ } if pages < 1 { pages = 1 } var guilds []GuildInfo if h.DiscordInfo != nil { guilds = h.DiscordInfo.GetGuildList() } h.Renderer.Page(w, "convocations", convocationsData{ baseData: base, Config: cfg, ConvocPanels: convocPanels, Tickets: tickets, Total: total, Page: page, Pages: pages, Guilds: guilds, }) } // HandlePOST serves POST /convocations — saves the convocation config. func (h *ConvocationsHandler) HandlePOST(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } // Load existing config to preserve panel_message_id (not sent by form) existing, _ := h.ConvocRepo.Get(ctx, h.GuildID) panelMsgID := "" panelChannelID := strings.TrimSpace(r.FormValue("panel_channel_id")) if existing != nil { panelMsgID = existing.PanelMessageID // If channel changed, clear stored message ID so SendConvocPanel sends a new message if existing.PanelChannelID != panelChannelID { panelMsgID = "" } } embedColor := strings.TrimSpace(r.FormValue("panel_embed_color")) if embedColor == "" { embedColor = "#5865f2" } cfg := &db.ConvocationConfig{ GuildID: h.GuildID, CategoryID: strings.TrimSpace(r.FormValue("category_id")), LogChannelID: strings.TrimSpace(r.FormValue("log_channel_id")), ModalEnabled: r.FormValue("modal_enabled") == "1", PanelChannelID: panelChannelID, PanelMessageID: panelMsgID, PanelEmbedTitle: strings.TrimSpace(r.FormValue("panel_embed_title")), PanelEmbedDescription: strings.TrimSpace(r.FormValue("panel_embed_description")), PanelEmbedColor: embedColor, } if err := h.ConvocRepo.Upsert(ctx, cfg); err != nil { http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/convocations?flash=Configuration+sauvegardée", http.StatusSeeOther) } // HandleCreatePanel serves POST /convocations/panels/new — creates a new convocation panel entry. func (h *ConvocationsHandler) HandleCreatePanel(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if h.ConvocPanelRepo == nil { http.Redirect(w, r, "/convocations?error=Non+configuré", http.StatusSeeOther) return } color := strings.TrimSpace(r.FormValue("embed_color")) if color == "" { color = "#5865f2" } p := &db.ConvocationPanel{ GuildID: h.GuildID, EmbedTitle: strings.TrimSpace(r.FormValue("embed_title")), EmbedDesc: strings.TrimSpace(r.FormValue("embed_description")), EmbedColor: color, ChannelID: strings.TrimSpace(r.FormValue("channel_id")), } if err := h.ConvocPanelRepo.Create(ctx, p); err != nil { http.Redirect(w, r, "/convocations?error="+urlEncode(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/convocations?flash=Panel+créé", http.StatusSeeOther) } // HandleDeletePanel serves POST /convocations/panels/{id}/delete. func (h *ConvocationsHandler) HandleDeletePanel(w http.ResponseWriter, r *http.Request) { ctx := r.Context() idStr := chi.URLParam(r, "id") var id int64 fmt.Sscanf(idStr, "%d", &id) if h.ConvocPanelRepo != nil { h.ConvocPanelRepo.Delete(ctx, id) //nolint } http.Redirect(w, r, "/convocations?flash=Panel+supprimé", http.StatusSeeOther) } // HandleSendPanel serves POST /convocations/panels/{id}/send. func (h *ConvocationsHandler) HandleSendPanel(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") var id int64 fmt.Sscanf(idStr, "%d", &id) if h.BotService == nil { http.Redirect(w, r, "/convocations?error=Bot+non+disponible", http.StatusSeeOther) return } if err := h.BotService.SendConvocPanelByID(id); err != nil { http.Redirect(w, r, "/convocations?error="+urlEncode(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/convocations?flash=Panel+envoyé+sur+Discord", http.StatusSeeOther) } // HandleCreate serves POST /convocations/create — creates a convocation via Discord. func (h *ConvocationsHandler) HandleCreate(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if h.BotService == nil { http.Error(w, "bot non disponible", http.StatusServiceUnavailable) return } guildID := strings.TrimSpace(r.FormValue("guild_id")) if guildID == "" { guildID = h.GuildID } targetID := strings.TrimSpace(r.FormValue("target_id")) reason := strings.TrimSpace(r.FormValue("reason")) if targetID == "" || reason == "" { http.Redirect(w, r, "/convocations?error=ID+utilisateur+et+raison+requis", http.StatusSeeOther) return } admin := panelauth.AdminFromContext(r.Context()) staffDiscordID := "" if admin != nil { staffDiscordID = admin.DiscordID } channelID, err := h.BotService.CreateConvocation(guildID, staffDiscordID, targetID, reason) if err != nil { http.Redirect(w, r, "/convocations?error="+urlEncode(err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/convocations?flash=Convocation+créée+dans+<#"+channelID+">", http.StatusSeeOther) } func urlEncode(s string) string { var out strings.Builder for _, r := range s { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9': out.WriteRune(r) case r == '-' || r == '_' || r == '.' || r == '~': out.WriteRune(r) default: out.WriteString("+" + strings.ReplaceAll(string(r), " ", "+")) } } return out.String() }