434 lines
13 KiB
Go
434 lines
13 KiB
Go
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
|
|
}
|