butterrobot/internal/admin/admin.go
Felipe M. 8801c43f92
All checks were successful
CI / goreleaser-lint (push) Successful in 10s
CI / format (push) Successful in 1m52s
Release / release (push) Successful in 5m40s
CI / lint (push) Successful in 2m31s
CI / test (push) Successful in 1m0s
CI / build (push) Successful in 4m6s
feat: add !gallerydl command to download media from links
Reply to a message containing a link with !gallerydl (or send it with a
link inline) to download the media via the gallery-dl CLI and have the
files posted back as a reply. Restricted to an allowed_users allowlist
(comma-separated, per-channel/global config); no one is allowed by
default.

- Add media support to the platform layer: model.Message.Files,
  ActionSendMedia, and Platform.SendMedia. Telegram uploads via
  multipart (sendPhoto/sendVideo/sendDocument, 50MB cap); Slack returns
  unsupported for now.
- Add internal/shell (os/exec wrapper) and internal/urlutil (URL
  extraction) helpers with tests.
- Dispatch ActionSendMedia in app and clean up the temp download dir.
- Admin config UI for allowed_users / gallery_dl_path; docs entry.
- Containerfile: base on alpine and install gallery-dl + ffmpeg (was
  scratch); release workflow sets up QEMU + Buildx for cross-arch image
  builds.
2026-07-05 20:05:57 +02:00

909 lines
24 KiB
Go

package admin
import (
"embed"
"encoding/gob"
"fmt"
"html/template"
"net/http"
"net/url"
"strconv"
"strings"
"git.nakama.town/fmartingr/butterrobot/internal/config"
"git.nakama.town/fmartingr/butterrobot/internal/db"
"git.nakama.town/fmartingr/butterrobot/internal/model"
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
"github.com/gorilla/sessions"
)
//go:embed templates/*.html templates/plugins/*.html
var templateFS embed.FS
const (
// Session store key
sessionKey = "butterrobot-session"
)
// FlashMessage represents a flash message
type FlashMessage struct {
Category string
Message string
}
func init() {
// Register the FlashMessage type with gob package for session serialization
gob.Register(FlashMessage{})
}
// TemplateData holds data for rendering templates
type TemplateData struct {
User *model.User
LoggedIn bool
Title string
Path string
Flash []FlashMessage
Plugins map[string]model.Plugin
Channels []*model.Channel
Channel *model.Channel
ChannelPlugin *model.ChannelPlugin
PluginID string
Config map[string]any
GlobalConfig map[string]any
Version string
}
// Admin represents the admin interface
type Admin struct {
config *config.Config
db *db.Database
store *sessions.CookieStore
templates map[string]*template.Template
baseTemplate *template.Template
version string
}
// New creates a new Admin instance
func New(cfg *config.Config, database *db.Database, version string) *Admin {
// Create session store with appropriate options
store := sessions.NewCookieStore([]byte(cfg.SecretKey))
store.Options = &sessions.Options{
Path: "/admin",
MaxAge: 3600 * 24 * 7, // 1 week
HttpOnly: true,
}
// Load templates
templates := make(map[string]*template.Template)
// Create a template function map with helper functions
funcMap := template.FuncMap{
"contains": strings.Contains,
}
// Read base template from embedded filesystem
baseContent, err := templateFS.ReadFile("templates/_base.html")
if err != nil {
panic(err)
}
// Create a custom template with functions
baseTemplate, err := template.New("_base.html").Funcs(funcMap).Parse(string(baseContent))
if err != nil {
panic(err)
}
// Parse and register all templates
mainTemplateFiles := []string{
"index.html",
"login.html",
"change_password.html",
"channel_list.html",
"channel_detail.html",
"plugin_list.html",
"channel_plugins_list.html",
"channel_plugin_config.html",
"global_plugin_config.html",
}
pluginTemplateFiles := []string{
"plugins/_fields.html",
"plugins/security.domainblock.html",
"plugins/social.instagram.html",
"plugins/social.twitter.html",
"plugins/util.gallerydl.html",
}
for _, tf := range mainTemplateFiles {
// Read template content from embedded filesystem
content, err := templateFS.ReadFile("templates/" + tf)
if err != nil {
panic(err)
}
// Create a clone of the base template
t, err := baseTemplate.Clone()
if err != nil {
panic(err)
}
// Parse the template content
t, err = t.Parse(string(content))
if err != nil {
panic(err)
}
// Plugin config pages share the per-plugin field partials
if tf == "channel_plugin_config.html" || tf == "global_plugin_config.html" {
for _, pluginTf := range pluginTemplateFiles {
pluginContent, err := templateFS.ReadFile("templates/" + pluginTf)
if err != nil {
panic(err)
}
t, err = t.Parse(string(pluginContent))
if err != nil {
panic(err)
}
}
}
templates[tf] = t
}
return &Admin{
config: cfg,
db: database,
store: store,
templates: templates,
baseTemplate: baseTemplate,
version: version,
}
}
// RegisterRoutes registers admin routes on the given router
func (a *Admin) RegisterRoutes(mux *http.ServeMux) {
// Register admin routes
mux.HandleFunc("/admin/", a.handleIndex)
mux.HandleFunc("/admin/login", a.handleLogin)
mux.HandleFunc("/admin/logout", a.handleLogout)
mux.HandleFunc("/admin/change-password", a.handleChangePassword)
mux.HandleFunc("/admin/plugins", a.handlePluginList)
mux.HandleFunc("/admin/plugins/config/", a.handleGlobalPluginConfig)
mux.HandleFunc("/admin/channels", a.handleChannelList)
mux.HandleFunc("/admin/channels/", a.handleChannelDetail)
mux.HandleFunc("/admin/channelplugins", a.handleChannelPluginList)
mux.HandleFunc("/admin/channelplugins/config/", a.handleChannelPluginConfig)
mux.HandleFunc("/admin/channelplugins/", a.handleChannelPluginDetailOrDelete)
}
// getCurrentUser gets the current user from the session
func (a *Admin) getCurrentUser(r *http.Request) *model.User {
session, err := a.store.Get(r, sessionKey)
if err != nil {
fmt.Printf("Error getting session for user retrieval: %v\n", err)
return nil
}
// Check if user is logged in
userID, ok := session.Values["user_id"].(int64)
if !ok {
return nil
}
// Get user from database
user, err := a.db.GetUserByID(userID)
if err != nil {
fmt.Printf("Error retrieving user from database: %v\n", err)
return nil
}
return user
}
// isLoggedIn checks if the user is logged in
func (a *Admin) isLoggedIn(r *http.Request) bool {
session, err := a.store.Get(r, sessionKey)
if err != nil {
fmt.Printf("Error getting session for login check: %v\n", err)
return false
}
return session.Values["logged_in"] == true
}
// addFlash adds a flash message to the session
func (a *Admin) addFlash(w http.ResponseWriter, r *http.Request, message string, category string) {
session, err := a.store.Get(r, sessionKey)
if err != nil {
// If there's an error getting the session, create a new one
session = sessions.NewSession(a.store, sessionKey)
session.Options = &sessions.Options{
Path: "/admin",
MaxAge: 3600 * 24 * 7, // 1 week
HttpOnly: true,
}
}
// Map internal categories to Bootstrap alert classes
var alertClass string
switch category {
case "success":
alertClass = "success"
case "danger":
alertClass = "danger"
case "warning":
alertClass = "warning"
case "info":
alertClass = "info"
default:
alertClass = "info"
}
flash := FlashMessage{
Category: alertClass,
Message: message,
}
session.AddFlash(flash)
err = session.Save(r, w)
if err != nil {
// Log the error or handle it appropriately
fmt.Printf("Error saving session: %v\n", err)
}
}
// getFlashes gets all flash messages from the session
func (a *Admin) getFlashes(w http.ResponseWriter, r *http.Request) []FlashMessage {
session, err := a.store.Get(r, sessionKey)
if err != nil {
// If there's an error getting the session, return an empty slice
fmt.Printf("Error getting session for flashes: %v\n", err)
return []FlashMessage{}
}
// Get flash messages
flashes := session.Flashes()
messages := make([]FlashMessage, 0, len(flashes))
for _, f := range flashes {
if flash, ok := f.(FlashMessage); ok {
messages = append(messages, flash)
}
}
// Save session to clear flashes
err = session.Save(r, w)
if err != nil {
fmt.Printf("Error saving session after getting flashes: %v\n", err)
}
return messages
}
// render renders a template with the given data
func (a *Admin) render(w http.ResponseWriter, r *http.Request, templateName string, data TemplateData) {
// Add current user data
data.User = a.getCurrentUser(r)
data.LoggedIn = a.isLoggedIn(r)
data.Path = r.URL.Path
data.Flash = a.getFlashes(w, r)
data.Version = a.version
// Get template
tmpl, ok := a.templates[templateName]
if !ok {
http.Error(w, "Template not found", http.StatusInternalServerError)
return
}
// Render template
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// handleIndex handles the admin index route
func (a *Admin) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/admin/" {
http.NotFound(w, r)
return
}
// Redirect to login if not logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Redirect to channel list
http.Redirect(w, r, "/admin/channels", http.StatusSeeOther)
}
// handleLogin handles the login route
func (a *Admin) handleLogin(w http.ResponseWriter, r *http.Request) {
// If already logged in, redirect to index
if a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
return
}
// Handle login form submission
if r.Method == http.MethodPost {
// Parse form
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Check credentials
username := r.FormValue("username")
password := r.FormValue("password")
user, err := a.db.CheckCredentials(username, password)
if err != nil || user == nil {
a.addFlash(w, r, "Incorrect credentials", "danger")
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Set session
session, _ := a.store.Get(r, sessionKey)
session.Values["logged_in"] = true
session.Values["user_id"] = user.ID
// Set session expiration
session.Options.MaxAge = 3600 * 24 * 7 // 1 week
err = session.Save(r, w)
if err != nil {
fmt.Printf("Error saving session: %v\n", err)
}
a.addFlash(w, r, "You were logged in", "success")
// Redirect to index
next := r.URL.Query().Get("next")
if next == "" {
next = "/admin/"
}
http.Redirect(w, r, next, http.StatusSeeOther)
return
}
// Render login template
a.render(w, r, "login.html", TemplateData{
Title: "Login",
})
}
// handleLogout handles the logout route
func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) {
// Clear session
session, err := a.store.Get(r, sessionKey)
if err != nil {
fmt.Printf("Error getting session for logout: %v\n", err)
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
session.Values = make(map[interface{}]interface{})
session.Options.MaxAge = -1 // Delete session
err = session.Save(r, w)
if err != nil {
fmt.Printf("Error saving session for logout: %v\n", err)
}
a.addFlash(w, r, "You were logged out", "success")
// Redirect to login
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
}
// handleChangePassword handles the change password route
func (a *Admin) handleChangePassword(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Get current user
user := a.getCurrentUser(r)
if user == nil {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Handle form submission
if r.Method == http.MethodPost {
// Parse form
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Get form values
currentPassword := r.FormValue("current_password")
newPassword := r.FormValue("new_password")
confirmPassword := r.FormValue("confirm_password")
// Validate current password
_, err := a.db.CheckCredentials(user.Username, currentPassword)
if err != nil {
a.addFlash(w, r, "Current password is incorrect", "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
// Validate new password and confirmation
if newPassword == "" {
a.addFlash(w, r, "New password cannot be empty", "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
if newPassword != confirmPassword {
a.addFlash(w, r, "New passwords do not match", "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
// Update password
if err := a.db.UpdateUserPassword(user.ID, newPassword); err != nil {
a.addFlash(w, r, "Failed to update password: "+err.Error(), "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
// Success
a.addFlash(w, r, "Password changed successfully", "success")
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
return
}
// Render change password template
a.render(w, r, "change_password.html", TemplateData{
Title: "Change Password",
})
}
// handlePluginList handles the plugin list route
func (a *Admin) handlePluginList(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Get available plugins
plugins := plugin.GetAvailablePlugins()
// Render template
a.render(w, r, "plugin_list.html", TemplateData{
Title: "Plugins",
Plugins: plugins,
})
}
// handleChannelList handles the channel list route
func (a *Admin) handleChannelList(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Get all channels
channels, err := a.db.GetAllChannels()
if err != nil {
http.Error(w, "Failed to get channels", http.StatusInternalServerError)
return
}
// Render template
a.render(w, r, "channel_list.html", TemplateData{
Title: "Channels",
Channels: channels,
})
}
// handleChannelDetail handles the channel detail route
func (a *Admin) handleChannelDetail(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Extract channel ID from path
path := r.URL.Path
if path == "/admin/channels/" {
http.Redirect(w, r, "/admin/channels", http.StatusSeeOther)
return
}
channelID := strings.TrimPrefix(path, "/admin/channels/")
if strings.Contains(channelID, "/") {
// Handle delete request
if strings.HasSuffix(path, "/delete") && r.Method == http.MethodPost {
channelID = strings.TrimSuffix(channelID, "/delete")
// Delete channel
id, err := strconv.ParseInt(channelID, 10, 64)
if err != nil {
http.Error(w, "Invalid channel ID", http.StatusBadRequest)
return
}
if err := a.db.DeleteChannel(id); err != nil {
http.Error(w, "Failed to delete channel", http.StatusInternalServerError)
return
}
a.addFlash(w, r, "Channel removed", "success")
http.Redirect(w, r, "/admin/channels", http.StatusSeeOther)
return
}
http.NotFound(w, r)
return
}
// Convert channel ID to int64
id, err := strconv.ParseInt(channelID, 10, 64)
if err != nil {
http.Error(w, "Invalid channel ID", http.StatusBadRequest)
return
}
// Handle form submission
if r.Method == http.MethodPost {
// Parse form
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Check if the form was submitted
if r.FormValue("form_submitted") == "true" {
// Update channel
enabled := r.FormValue("enabled") == "true"
if err := a.db.UpdateChannel(id, enabled); err != nil {
http.Error(w, "Failed to update channel", http.StatusInternalServerError)
return
}
// Update enable_all_plugins
enableAllPlugins := r.FormValue("enable_all_plugins") == "true"
if err := a.db.UpdateChannelEnableAllPlugins(id, enableAllPlugins); err != nil {
http.Error(w, "Failed to update channel enable all plugins", http.StatusInternalServerError)
return
}
a.addFlash(w, r, "Channel updated", "success")
http.Redirect(w, r, "/admin/channels/"+channelID, http.StatusSeeOther)
return
}
}
// Get channel
channel, err := a.db.GetChannelByID(id)
if err != nil {
http.Error(w, "Channel not found", http.StatusNotFound)
return
}
// Get available plugins
plugins := plugin.GetAvailablePlugins()
// Render template
a.render(w, r, "channel_detail.html", TemplateData{
Title: "Channel: " + channel.PlatformChannelID,
Channel: channel,
Plugins: plugins,
})
}
// handleChannelPluginList handles the channel plugin list route
func (a *Admin) handleChannelPluginList(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Handle form submission
if r.Method == http.MethodPost {
// Parse form
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Extract form data
channelID, err := strconv.ParseInt(r.FormValue("channel_id"), 10, 64)
if err != nil {
http.Error(w, "Invalid channel ID", http.StatusBadRequest)
return
}
pluginID := r.FormValue("plugin_id")
enabled := r.FormValue("enabled") == "y"
// Create channel plugin
config := make(map[string]interface{})
_, err = a.db.CreateChannelPlugin(channelID, pluginID, enabled, config)
if err == db.ErrDuplicated {
a.addFlash(w, r, "Plugin "+pluginID+" is already present on the channel", "danger")
} else if err != nil {
http.Error(w, "Failed to create channel plugin", http.StatusInternalServerError)
return
} else {
a.addFlash(w, r, "Plugin "+pluginID+" added to the channel", "success")
}
// Redirect back
referer := r.Header.Get("Referer")
if referer == "" {
referer = "/admin/channelplugins"
}
http.Redirect(w, r, referer, http.StatusSeeOther)
return
}
// Get all channels
channels, err := a.db.GetAllChannels()
if err != nil {
http.Error(w, "Failed to get channels", http.StatusInternalServerError)
return
}
// Render template
a.render(w, r, "channel_plugins_list.html", TemplateData{
Title: "Channel Plugins",
Channels: channels,
Plugins: plugin.GetAvailablePlugins(),
})
}
// handleChannelPluginConfig handles the channel plugin configuration route
func (a *Admin) handleChannelPluginConfig(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Extract channel plugin ID from path
path := r.URL.Path
channelPluginID := strings.TrimPrefix(path, "/admin/channelplugins/config/")
// Convert channel plugin ID to int64
id, err := strconv.ParseInt(channelPluginID, 10, 64)
if err != nil {
http.Error(w, "Invalid channel plugin ID", http.StatusBadRequest)
return
}
// Get the channel plugin
channelPlugin, err := a.db.GetChannelPluginByID(id)
if err != nil {
http.Error(w, "Channel plugin not found", http.StatusNotFound)
return
}
// Get the plugin
p, err := plugin.Get(channelPlugin.PluginID)
if err != nil {
http.Error(w, "Plugin not found", http.StatusNotFound)
return
}
// Handle form submission
if r.Method == http.MethodPost {
// Parse form
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Create config map from form values
config := buildPluginConfigFromForm(channelPlugin.PluginID, r.Form)
// Update plugin configuration
if err := a.db.UpdateChannelPluginConfig(id, config); err != nil {
http.Error(w, "Failed to update plugin configuration", http.StatusInternalServerError)
return
}
// Get the channel to redirect back to the channel detail page
channel, err := a.db.GetChannelByID(channelPlugin.ChannelID)
if err != nil {
a.addFlash(w, r, "Plugin configuration updated", "success")
http.Redirect(w, r, "/admin/channelplugins", http.StatusSeeOther)
return
}
a.addFlash(w, r, "Plugin configuration updated", "success")
http.Redirect(w, r, fmt.Sprintf("/admin/channels/%d", channel.ID), http.StatusSeeOther)
return
}
// Load the global configuration so the form can show inherited values
globalConfig, err := a.db.GetGlobalPluginConfig(channelPlugin.PluginID)
if err != nil {
http.Error(w, "Failed to get global plugin configuration", http.StatusInternalServerError)
return
}
// Render template
a.render(w, r, "channel_plugin_config.html", TemplateData{
Title: "Configure Plugin: " + p.GetName(),
ChannelPlugin: channelPlugin,
PluginID: channelPlugin.PluginID,
Config: channelPlugin.Config,
GlobalConfig: globalConfig,
Plugins: map[string]model.Plugin{channelPlugin.PluginID: p},
})
}
// buildPluginConfigFromForm builds a plugin configuration map from submitted form
// values. Most plugins use generic key/value handling; a few need special casing.
func buildPluginConfigFromForm(pluginID string, form url.Values) map[string]any {
config := make(map[string]any)
switch pluginID {
case "security.domainblock":
config["blocked_domains"] = form.Get("blocked_domains")
default:
for key, values := range form {
if key == "form_submitted" {
continue
}
if len(values) == 1 {
config[key] = values[0]
} else {
config[key] = values
}
}
}
return config
}
// handleGlobalPluginConfig handles the global (app-wide) plugin configuration route
func (a *Admin) handleGlobalPluginConfig(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Extract plugin ID from path
pluginID := strings.TrimPrefix(r.URL.Path, "/admin/plugins/config/")
if pluginID == "" {
http.Redirect(w, r, "/admin/plugins", http.StatusSeeOther)
return
}
// Get the plugin
p, err := plugin.Get(pluginID)
if err != nil {
http.Error(w, "Plugin not found", http.StatusNotFound)
return
}
// Handle form submission
if r.Method == http.MethodPost {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
config := buildPluginConfigFromForm(pluginID, r.Form)
if err := a.db.SetGlobalPluginConfig(pluginID, config); err != nil {
http.Error(w, "Failed to update global plugin configuration", http.StatusInternalServerError)
return
}
a.addFlash(w, r, "Global plugin configuration updated", "success")
http.Redirect(w, r, "/admin/plugins", http.StatusSeeOther)
return
}
// Load current global configuration
config, err := a.db.GetGlobalPluginConfig(pluginID)
if err != nil {
http.Error(w, "Failed to get global plugin configuration", http.StatusInternalServerError)
return
}
// Render template
a.render(w, r, "global_plugin_config.html", TemplateData{
Title: "Global configuration: " + p.GetName(),
PluginID: pluginID,
Config: config,
Plugins: map[string]model.Plugin{pluginID: p},
})
}
// handleChannelPluginDetailOrDelete handles the channel plugin detail or delete route
func (a *Admin) handleChannelPluginDetailOrDelete(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Extract channel plugin ID from path
path := r.URL.Path
if path == "/admin/channelplugins/" {
http.Redirect(w, r, "/admin/channelplugins", http.StatusSeeOther)
return
}
channelPluginID := strings.TrimPrefix(path, "/admin/channelplugins/")
// Handle delete request
if strings.HasSuffix(channelPluginID, "/delete") && r.Method == http.MethodPost {
channelPluginID = strings.TrimSuffix(channelPluginID, "/delete")
// Delete channel plugin
id, err := strconv.ParseInt(channelPluginID, 10, 64)
if err != nil {
http.Error(w, "Invalid channel plugin ID", http.StatusBadRequest)
return
}
if err := a.db.DeleteChannelPlugin(id); err != nil {
http.Error(w, "Failed to delete channel plugin", http.StatusInternalServerError)
return
}
a.addFlash(w, r, "Plugin removed", "success")
// Redirect back
referer := r.Header.Get("Referer")
if referer == "" {
referer = "/admin/channelplugins"
}
http.Redirect(w, r, referer, http.StatusSeeOther)
return
}
// Handle update request
if r.Method == http.MethodPost {
// Parse form
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Convert channel plugin ID to int64
id, err := strconv.ParseInt(channelPluginID, 10, 64)
if err != nil {
http.Error(w, "Invalid channel plugin ID", http.StatusBadRequest)
return
}
// Update channel plugin
enabled := r.FormValue("enabled") == "true"
if err := a.db.UpdateChannelPlugin(id, enabled); err != nil {
http.Error(w, "Failed to update channel plugin", http.StatusInternalServerError)
return
}
a.addFlash(w, r, "Plugin updated", "success")
// Redirect back
referer := r.Header.Get("Referer")
if referer == "" {
referer = "/admin/channelplugins"
}
http.Redirect(w, r, referer, http.StatusSeeOther)
return
}
// Redirect to channel plugins list
http.Redirect(w, r, "/admin/channelplugins", http.StatusSeeOther)
}