mattermost-plugin-cleanup-c.../server/api.go
Felipe M. 85e2a38f17
Some checks failed
ci / test (push) Successful in 3m7s
ci / lint (push) Failing after 2m50s
ci / build (push) Successful in 4m21s
release / release (push) Successful in 5m43s
Add automatic channel cleanup with unified modal UI.
Per-channel retention config, daily scheduled cleanup, slash command and REST API support, plus a single Channel cleanup menu that opens a plugin modal for configure, run-now, and disable actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 18:35:24 +02:00

243 lines
7.2 KiB
Go

package main
import (
"encoding/json"
"errors"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost/server/public/plugin"
)
type cleanupRequest struct {
ChannelID string `json:"channel_id"`
}
type cleanupResponse struct {
Deleted int `json:"deleted"`
}
type autoCleanupRequest struct {
ChannelID string `json:"channel_id"`
OffsetDays int `json:"offset_days"`
}
type autoCleanupResponse struct {
ChannelID string `json:"channel_id"`
OffsetDays int `json:"offset_days"`
Enabled bool `json:"enabled"`
}
func (p *Plugin) initRouter() *mux.Router {
router := mux.NewRouter()
router.Use(p.requireLoggedInUser)
apiRouter := router.PathPrefix("/api/v1").Subrouter()
apiRouter.HandleFunc("/cleanup", p.handleCleanup).Methods(http.MethodPost)
apiRouter.HandleFunc("/auto-cleanup/run", p.handleRunAutoCleanup).Methods(http.MethodPost)
apiRouter.HandleFunc("/auto-cleanup", p.handleSetAutoCleanup).Methods(http.MethodPut)
apiRouter.HandleFunc("/auto-cleanup/{channel_id}", p.handleGetAutoCleanup).Methods(http.MethodGet)
apiRouter.HandleFunc("/auto-cleanup/{channel_id}", p.handleDeleteAutoCleanup).Methods(http.MethodDelete)
return router
}
func (p *Plugin) ServeHTTP(_ *plugin.Context, w http.ResponseWriter, r *http.Request) {
p.router.ServeHTTP(w, r)
}
func (p *Plugin) requireLoggedInUser(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Mattermost-User-ID") == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func (p *Plugin) handleCleanup(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
var req cleanupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.ChannelID == "" {
http.Error(w, "channel_id is required", http.StatusBadRequest)
return
}
deleted, err := p.cleanupChannel(userID, req.ChannelID)
if err != nil {
if errors.Is(err, errPermissionDenied) {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
p.client.Log.Error("Failed to cleanup channel", "channel_id", req.ChannelID, "error", err)
http.Error(w, "Failed to cleanup channel", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(cleanupResponse{Deleted: deleted}); err != nil {
p.client.Log.Error("Failed to encode cleanup response", "error", err)
}
}
type autoCleanupRunRequest struct {
ChannelID string `json:"channel_id"`
OffsetDays int `json:"offset_days,omitempty"`
}
func (p *Plugin) handleRunAutoCleanup(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if !p.isSystemAdmin(userID) {
http.Error(w, errPermissionDenied.Error(), http.StatusForbidden)
return
}
var req autoCleanupRunRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.ChannelID == "" {
http.Error(w, "channel_id is required", http.StatusBadRequest)
return
}
offsetDays := req.OffsetDays
if offsetDays <= 0 {
config, err := p.autoCleanupStore.Get(req.ChannelID)
if err != nil {
p.client.Log.Error("Failed to get auto cleanup config", "channel_id", req.ChannelID, "error", err)
http.Error(w, "Failed to get auto cleanup config", http.StatusInternalServerError)
return
}
if config == nil {
http.Error(w, "automatic cleanup is not configured for this channel", http.StatusBadRequest)
return
}
offsetDays = config.OffsetDays
}
deleted, err := p.autoCleanupChannel(req.ChannelID, offsetDays)
if err != nil {
p.client.Log.Error("Failed to run auto cleanup", "channel_id", req.ChannelID, "error", err)
http.Error(w, "Failed to run auto cleanup", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(cleanupResponse{Deleted: deleted}); err != nil {
p.client.Log.Error("Failed to encode auto cleanup run response", "error", err)
}
}
func (p *Plugin) handleGetAutoCleanup(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if !p.isSystemAdmin(userID) {
http.Error(w, errPermissionDenied.Error(), http.StatusForbidden)
return
}
channelID := mux.Vars(r)["channel_id"]
if channelID == "" {
http.Error(w, "channel_id is required", http.StatusBadRequest)
return
}
config, err := p.autoCleanupStore.Get(channelID)
if err != nil {
p.client.Log.Error("Failed to get auto cleanup config", "channel_id", channelID, "error", err)
http.Error(w, "Failed to get auto cleanup config", http.StatusInternalServerError)
return
}
response := autoCleanupResponse{
ChannelID: channelID,
Enabled: config != nil,
}
if config != nil {
response.OffsetDays = config.OffsetDays
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
p.client.Log.Error("Failed to encode auto cleanup response", "error", err)
}
}
func (p *Plugin) handleSetAutoCleanup(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if !p.isSystemAdmin(userID) {
http.Error(w, errPermissionDenied.Error(), http.StatusForbidden)
return
}
var req autoCleanupRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.ChannelID == "" {
http.Error(w, "channel_id is required", http.StatusBadRequest)
return
}
if req.OffsetDays <= 0 {
http.Error(w, "offset_days must be greater than zero", http.StatusBadRequest)
return
}
if _, err := p.client.Channel.Get(req.ChannelID); err != nil {
http.Error(w, "channel not found", http.StatusNotFound)
return
}
if err := p.autoCleanupStore.Set(req.ChannelID, req.OffsetDays); err != nil {
p.client.Log.Error("Failed to set auto cleanup config", "channel_id", req.ChannelID, "error", err)
http.Error(w, "Failed to set auto cleanup config", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(autoCleanupResponse{
ChannelID: req.ChannelID,
OffsetDays: req.OffsetDays,
Enabled: true,
}); err != nil {
p.client.Log.Error("Failed to encode auto cleanup response", "error", err)
}
}
func (p *Plugin) handleDeleteAutoCleanup(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if !p.isSystemAdmin(userID) {
http.Error(w, errPermissionDenied.Error(), http.StatusForbidden)
return
}
channelID := mux.Vars(r)["channel_id"]
if channelID == "" {
http.Error(w, "channel_id is required", http.StatusBadRequest)
return
}
if err := p.autoCleanupStore.Delete(channelID); err != nil {
p.client.Log.Error("Failed to delete auto cleanup config", "channel_id", channelID, "error", err)
http.Error(w, "Failed to delete auto cleanup config", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(autoCleanupResponse{
ChannelID: channelID,
Enabled: false,
}); err != nil {
p.client.Log.Error("Failed to encode auto cleanup response", "error", err)
}
}