oups
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/leolionad58/ticketbot/internal/db"
|
||||
panelauth "github.com/leolionad58/ticketbot/internal/panel/auth"
|
||||
)
|
||||
|
||||
// BotSender is the subset of BotService used by PanelsHandler.
|
||||
type BotSender interface {
|
||||
SendPanel(panelID int64) error
|
||||
DeletePanelMessage(panelID int64) error
|
||||
}
|
||||
|
||||
// GuildRole is a Discord role passed to the panel form.
|
||||
type GuildRole struct {
|
||||
ID string
|
||||
Name string
|
||||
Color string
|
||||
}
|
||||
|
||||
// GuildChannel is a Discord channel/category passed to the panel form.
|
||||
type GuildChannel struct {
|
||||
ID string
|
||||
Name string
|
||||
Type int // 0=text, 2=voice, 4=category
|
||||
}
|
||||
|
||||
// GuildInfo is a minimal guild descriptor for the guild selector.
|
||||
type GuildInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// DiscordInfo can fetch guild roles and channels for the form dropdowns.
|
||||
type DiscordInfo interface {
|
||||
GetGuildList() []GuildInfo
|
||||
GetGuildRoles(guildID string) ([]GuildRole, error)
|
||||
GetGuildChannels(guildID string) ([]GuildChannel, error)
|
||||
}
|
||||
|
||||
// PanelsHandler serves all /panels/* routes.
|
||||
type PanelsHandler struct {
|
||||
PanelRepo *db.PanelConfigRepo
|
||||
AuditLog *db.AuditLogRepo
|
||||
BotService BotSender
|
||||
DiscordInfo DiscordInfo
|
||||
Renderer *Renderer
|
||||
}
|
||||
|
||||
// HandleList serves GET /panels.
|
||||
func (h *PanelsHandler) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
base := h.Renderer.base(r, "panels")
|
||||
sess := panelauth.SessionFromContext(ctx)
|
||||
if sess != nil {
|
||||
base.CSRFToken = sess.CSRFToken
|
||||
}
|
||||
panels, _ := h.PanelRepo.List(ctx)
|
||||
|
||||
guildNames := map[string]string{}
|
||||
if h.DiscordInfo != nil {
|
||||
for _, g := range h.DiscordInfo.GetGuildList() {
|
||||
guildNames[g.ID] = g.Name
|
||||
}
|
||||
}
|
||||
|
||||
h.Renderer.Page(w, "panels_list", struct {
|
||||
baseData
|
||||
Panels []*db.PanelConfig
|
||||
GuildNames map[string]string
|
||||
}{baseData: base, Panels: panels, GuildNames: guildNames})
|
||||
}
|
||||
|
||||
// HandleNew serves GET /panels/new.
|
||||
func (h *PanelsHandler) HandleNew(w http.ResponseWriter, r *http.Request) {
|
||||
base := h.Renderer.base(r, "panels")
|
||||
guildID := r.URL.Query().Get("guild_id")
|
||||
if guildID == "" && h.DiscordInfo != nil {
|
||||
if guilds := h.DiscordInfo.GetGuildList(); len(guilds) > 0 {
|
||||
guildID = guilds[0].ID
|
||||
}
|
||||
}
|
||||
h.renderForm(w, base, &db.PanelConfig{EmbedColor: "#5865f2", GuildID: guildID}, true, nil)
|
||||
}
|
||||
|
||||
// HandleCreate serves POST /panels/new.
|
||||
func (h *PanelsHandler) HandleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
base := h.Renderer.base(r, "panels")
|
||||
sess := panelauth.SessionFromContext(ctx)
|
||||
if sess != nil {
|
||||
base.CSRFToken = sess.CSRFToken
|
||||
}
|
||||
|
||||
p, types, errs := parsePanel(r)
|
||||
if len(errs) > 0 {
|
||||
p.Types = types
|
||||
h.renderForm(w, base, p, true, errs)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.PanelRepo.Create(ctx, p); err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE") {
|
||||
h.renderForm(w, base, p, true, map[string]string{"name": "Ce nom est déjà utilisé."})
|
||||
return
|
||||
}
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(types) > 0 {
|
||||
if err := h.PanelRepo.ReplaceTypes(ctx, p.ID, types); err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.FormValue("action") == "save_send" && h.BotService != nil {
|
||||
_ = h.BotService.SendPanel(p.ID)
|
||||
}
|
||||
http.Redirect(w, r, "/panels", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// HandleEdit serves GET /panels/{id}/edit.
|
||||
func (h *PanelsHandler) HandleEdit(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
p, err := h.PanelRepo.GetByID(ctx, id)
|
||||
if err != nil || p == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
base := h.Renderer.base(r, "panels")
|
||||
sess := panelauth.SessionFromContext(ctx)
|
||||
if sess != nil {
|
||||
base.CSRFToken = sess.CSRFToken
|
||||
}
|
||||
h.renderForm(w, base, p, false, nil)
|
||||
}
|
||||
|
||||
// HandleUpdate serves POST /panels/{id}/edit.
|
||||
func (h *PanelsHandler) HandleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
base := h.Renderer.base(r, "panels")
|
||||
sess := panelauth.SessionFromContext(ctx)
|
||||
if sess != nil {
|
||||
base.CSRFToken = sess.CSRFToken
|
||||
}
|
||||
|
||||
p, types, errs := parsePanel(r)
|
||||
p.ID = id
|
||||
if len(errs) > 0 {
|
||||
p.Types = types
|
||||
h.renderForm(w, base, p, false, errs)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.PanelRepo.Update(ctx, p); err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := h.PanelRepo.ReplaceTypes(ctx, id, types); err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.FormValue("action") == "save_send" && h.BotService != nil {
|
||||
_ = h.BotService.SendPanel(id)
|
||||
}
|
||||
http.Redirect(w, r, "/panels", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// HandleDelete serves POST /panels/{id}/delete.
|
||||
func (h *PanelsHandler) HandleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err := h.PanelRepo.Delete(ctx, id); err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/panels", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// HandleDuplicate serves POST /panels/{id}/duplicate.
|
||||
func (h *PanelsHandler) HandleDuplicate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
src, err := h.PanelRepo.GetByID(ctx, id)
|
||||
if err != nil || src == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
copyP := &db.PanelConfig{
|
||||
Name: src.Name + "-copie",
|
||||
EmbedTitle: src.EmbedTitle,
|
||||
EmbedDescription: src.EmbedDescription,
|
||||
EmbedColor: src.EmbedColor,
|
||||
GuildID: src.GuildID,
|
||||
}
|
||||
if err := h.PanelRepo.Create(ctx, copyP); err != nil {
|
||||
http.Error(w, "duplicate failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Copy types (without IDs so they get fresh ones)
|
||||
var newTypes []*db.PanelType
|
||||
for _, t := range src.Types {
|
||||
newTypes = append(newTypes, &db.PanelType{
|
||||
Name: t.Name, ButtonLabel: t.ButtonLabel, ButtonColor: t.ButtonColor,
|
||||
ButtonEmoji: t.ButtonEmoji, EmbedColor: t.EmbedColor, EmbedTitle: t.EmbedTitle,
|
||||
EmbedText: t.EmbedText, StaffRoleID: t.StaffRoleID, CategoryID: t.CategoryID,
|
||||
ClaimChannelID: t.ClaimChannelID, LogChannelID: t.LogChannelID,
|
||||
MaxPerUser: t.MaxPerUser, ClaimMode: t.ClaimMode, CloseRule: t.CloseRule,
|
||||
ModalEnabled: t.ModalEnabled, SortOrder: t.SortOrder,
|
||||
})
|
||||
}
|
||||
if len(newTypes) > 0 {
|
||||
_ = h.PanelRepo.ReplaceTypes(ctx, copyP.ID, newTypes)
|
||||
}
|
||||
http.Redirect(w, r, fmt.Sprintf("/panels/%d/edit", copyP.ID), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// HandlePreview serves POST /panels/preview — returns an HTML fragment.
|
||||
func (h *PanelsHandler) HandlePreview(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
color := r.FormValue("embed_color")
|
||||
if color == "" {
|
||||
color = "#5865f2"
|
||||
}
|
||||
title := r.FormValue("embed_title")
|
||||
if title == "" {
|
||||
title = "Titre du panel"
|
||||
}
|
||||
desc := r.FormValue("embed_description")
|
||||
|
||||
type btn struct{ Label, Color, Emoji string }
|
||||
var buttons []btn
|
||||
for i := 0; i < 25; i++ {
|
||||
label := r.FormValue(fmt.Sprintf("types[%d][button_label]", i))
|
||||
if label == "" {
|
||||
break
|
||||
}
|
||||
buttons = append(buttons, btn{
|
||||
Label: label,
|
||||
Color: r.FormValue(fmt.Sprintf("types[%d][button_color]", i)),
|
||||
Emoji: r.FormValue(fmt.Sprintf("types[%d][button_emoji]", i)),
|
||||
})
|
||||
}
|
||||
|
||||
btnColorMap := map[string]string{
|
||||
"primary": "bg-indigo-600", "secondary": "bg-gray-600",
|
||||
"success": "bg-green-600", "danger": "bg-red-600",
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, `<div class="rounded-lg overflow-hidden border-l-4 bg-[#2b2d31] p-4" style="border-left-color:%s">`, color)
|
||||
fmt.Fprintf(w, `<p class="font-semibold text-white mb-1">%s</p>`, title)
|
||||
if desc != "" {
|
||||
fmt.Fprintf(w, `<p class="text-gray-300 text-sm mb-3">%s</p>`, desc)
|
||||
}
|
||||
if len(buttons) > 0 {
|
||||
fmt.Fprint(w, `<div class="flex flex-wrap gap-2 mt-2">`)
|
||||
for _, b := range buttons {
|
||||
cls := btnColorMap[b.Color]
|
||||
if cls == "" {
|
||||
cls = "bg-indigo-600"
|
||||
}
|
||||
fmt.Fprintf(w, `<span class="%s text-white text-sm px-3 py-1.5 rounded">%s %s</span>`, cls, b.Emoji, b.Label)
|
||||
}
|
||||
fmt.Fprint(w, `</div>`)
|
||||
}
|
||||
fmt.Fprint(w, `</div>`)
|
||||
}
|
||||
|
||||
// HandleReorderTypes serves POST /panels/{id}/types/reorder.
|
||||
func (h *PanelsHandler) HandleReorderTypes(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var ids []int64
|
||||
for _, s := range r.Form["ids[]"] {
|
||||
n, _ := strconv.ParseInt(s, 10, 64)
|
||||
ids = append(ids, n)
|
||||
}
|
||||
if err := h.PanelRepo.ReorderTypes(ctx, id, ids); err != nil {
|
||||
http.Error(w, "reorder failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *PanelsHandler) renderForm(w http.ResponseWriter, base baseData, p *db.PanelConfig, isNew bool, errs map[string]string) {
|
||||
if errs == nil {
|
||||
errs = map[string]string{}
|
||||
}
|
||||
var roles []GuildRole
|
||||
var textChans, categories []GuildChannel
|
||||
var guilds []GuildInfo
|
||||
if h.DiscordInfo != nil {
|
||||
guilds = h.DiscordInfo.GetGuildList()
|
||||
gid := p.GuildID
|
||||
if gid == "" && len(guilds) > 0 {
|
||||
gid = guilds[0].ID
|
||||
p.GuildID = gid
|
||||
}
|
||||
if gid != "" {
|
||||
roles, _ = h.DiscordInfo.GetGuildRoles(gid)
|
||||
chans, _ := h.DiscordInfo.GetGuildChannels(gid)
|
||||
for _, c := range chans {
|
||||
switch c.Type {
|
||||
case 0:
|
||||
textChans = append(textChans, c)
|
||||
case 4:
|
||||
categories = append(categories, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.Renderer.Page(w, "panel_form", struct {
|
||||
baseData
|
||||
Panel *db.PanelConfig
|
||||
IsNew bool
|
||||
Errors map[string]string
|
||||
Guilds []GuildInfo
|
||||
Roles []GuildRole
|
||||
TextChans []GuildChannel
|
||||
Categories []GuildChannel
|
||||
}{
|
||||
baseData: base,
|
||||
Panel: p,
|
||||
IsNew: isNew,
|
||||
Errors: errs,
|
||||
Guilds: guilds,
|
||||
Roles: roles,
|
||||
TextChans: textChans,
|
||||
Categories: categories,
|
||||
})
|
||||
}
|
||||
|
||||
// parsePanel extracts a PanelConfig + types from the posted form.
|
||||
func parsePanel(r *http.Request) (*db.PanelConfig, []*db.PanelType, map[string]string) {
|
||||
errs := map[string]string{}
|
||||
p := &db.PanelConfig{
|
||||
Name: strings.TrimSpace(r.FormValue("name")),
|
||||
EmbedTitle: strings.TrimSpace(r.FormValue("embed_title")),
|
||||
EmbedDescription: db.NullStr(strings.TrimSpace(r.FormValue("embed_description"))),
|
||||
EmbedColor: strings.TrimSpace(r.FormValue("embed_color")),
|
||||
EmbedImage: strings.TrimSpace(r.FormValue("embed_image")),
|
||||
EmbedThumbnail: strings.TrimSpace(r.FormValue("embed_thumbnail")),
|
||||
GuildID: strings.TrimSpace(r.FormValue("guild_id")),
|
||||
ChannelID: db.NullStr(strings.TrimSpace(r.FormValue("channel_id"))),
|
||||
}
|
||||
if p.Name == "" {
|
||||
errs["name"] = "Le nom est requis."
|
||||
}
|
||||
if p.EmbedTitle == "" {
|
||||
errs["embed_title"] = "Le titre est requis."
|
||||
}
|
||||
if p.EmbedColor == "" {
|
||||
p.EmbedColor = "#5865f2"
|
||||
}
|
||||
|
||||
var types []*db.PanelType
|
||||
for i := 0; i < 25; i++ {
|
||||
prefix := fmt.Sprintf("types[%d]", i)
|
||||
name := strings.TrimSpace(r.FormValue(prefix + "[name]"))
|
||||
if name == "" {
|
||||
break
|
||||
}
|
||||
claimMode := 0
|
||||
if r.FormValue(prefix+"[claim_mode]") == "1" {
|
||||
claimMode = 1
|
||||
}
|
||||
claimReupMinutes, _ := strconv.Atoi(r.FormValue(prefix + "[claim_reup_minutes]"))
|
||||
modalEnabled := r.FormValue(prefix+"[modal_enabled]") != "0"
|
||||
closeRule := r.FormValue(prefix + "[close_rule]")
|
||||
if closeRule == "" {
|
||||
closeRule = "staff_only"
|
||||
}
|
||||
maxPU, _ := strconv.Atoi(r.FormValue(prefix + "[max_per_user]"))
|
||||
if maxPU <= 0 {
|
||||
maxPU = 1
|
||||
}
|
||||
embedColor := strings.TrimSpace(r.FormValue(prefix + "[embed_color]"))
|
||||
if embedColor == "" {
|
||||
embedColor = "#5865f2"
|
||||
}
|
||||
buttonColor := strings.TrimSpace(r.FormValue(prefix + "[button_color]"))
|
||||
if buttonColor == "" {
|
||||
buttonColor = "primary"
|
||||
}
|
||||
types = append(types, &db.PanelType{
|
||||
Name: name,
|
||||
ButtonLabel: strings.TrimSpace(r.FormValue(prefix + "[button_label]")),
|
||||
ButtonColor: buttonColor,
|
||||
ButtonEmoji: strings.TrimSpace(r.FormValue(prefix + "[button_emoji]")),
|
||||
EmbedColor: embedColor,
|
||||
EmbedTitle: strings.TrimSpace(r.FormValue(prefix + "[embed_title]")),
|
||||
EmbedText: strings.TrimSpace(r.FormValue(prefix + "[embed_text]")),
|
||||
ThumbnailURL: strings.TrimSpace(r.FormValue(prefix + "[thumbnail_url]")),
|
||||
ImageURL: strings.TrimSpace(r.FormValue(prefix + "[image_url]")),
|
||||
StaffRoleID: strings.TrimSpace(r.FormValue(prefix + "[staff_role_id]")),
|
||||
CategoryID: strings.TrimSpace(r.FormValue(prefix + "[category_id]")),
|
||||
ClaimChannelID: strings.TrimSpace(r.FormValue(prefix + "[claim_channel_id]")),
|
||||
LogChannelID: strings.TrimSpace(r.FormValue(prefix + "[log_channel_id]")),
|
||||
MaxPerUser: maxPU,
|
||||
ClaimMode: claimMode,
|
||||
ClaimReupMinutes: claimReupMinutes,
|
||||
CloseRule: closeRule,
|
||||
ModalEnabled: modalEnabled,
|
||||
SortOrder: i,
|
||||
})
|
||||
}
|
||||
p.Types = types
|
||||
return p, types, errs
|
||||
}
|
||||
Reference in New Issue
Block a user