Offset-based pagination advanced after each page even though deletions shift remaining posts, so large cleanups silently left messages behind. Also page through all auto-cleanup KV keys and log individual delete failures. Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
2.5 KiB
Go
96 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
pkgerrors "github.com/pkg/errors"
|
|
)
|
|
|
|
var errPermissionDenied = errors.New("permission denied: system admin required")
|
|
|
|
const postsPerPage = 200
|
|
|
|
type postService interface {
|
|
GetPostsForChannel(channelID string, page, perPage int) (*model.PostList, error)
|
|
DeletePost(postID string) error
|
|
}
|
|
|
|
type errorLogger interface {
|
|
Error(message string, keyValuePairs ...any)
|
|
}
|
|
|
|
func (p *Plugin) isSystemAdmin(userID string) bool {
|
|
return p.client.User.HasPermissionTo(userID, model.PermissionManageSystem)
|
|
}
|
|
|
|
func deleteChannelPosts(postSvc postService, channelID string, olderThanMillis int64, log errorLogger) (int, error) {
|
|
deleted := 0
|
|
page := 0
|
|
|
|
for {
|
|
postList, err := postSvc.GetPostsForChannel(channelID, page, postsPerPage)
|
|
if err != nil {
|
|
return deleted, pkgerrors.Wrap(err, "failed to get channel posts")
|
|
}
|
|
if len(postList.Order) == 0 {
|
|
break
|
|
}
|
|
|
|
// Deleting posts shifts later results into earlier page offsets. Only advance
|
|
// the page when nothing was removed so we walk past posts that should be kept
|
|
// (e.g. messages newer than an auto-cleanup cutoff).
|
|
deletedOnPage := 0
|
|
for _, postID := range postList.Order {
|
|
post, ok := postList.Posts[postID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if olderThanMillis > 0 && post.CreateAt >= olderThanMillis {
|
|
continue
|
|
}
|
|
|
|
if err := postSvc.DeletePost(postID); err != nil {
|
|
if log != nil {
|
|
log.Error("Failed to delete post during channel cleanup", "channel_id", channelID, "post_id", postID, "error", err)
|
|
}
|
|
continue
|
|
}
|
|
deleted++
|
|
deletedOnPage++
|
|
}
|
|
|
|
if deletedOnPage == 0 {
|
|
page++
|
|
}
|
|
}
|
|
|
|
return deleted, nil
|
|
}
|
|
|
|
func (p *Plugin) cleanupChannel(userID, channelID string) (int, error) {
|
|
if !p.isSystemAdmin(userID) {
|
|
return 0, errPermissionDenied
|
|
}
|
|
|
|
if _, err := p.client.Channel.Get(channelID); err != nil {
|
|
return 0, pkgerrors.Wrap(err, "failed to get channel")
|
|
}
|
|
|
|
return deleteChannelPosts(&p.client.Post, channelID, 0, &p.client.Log)
|
|
}
|
|
|
|
func (p *Plugin) autoCleanupChannel(channelID string, offsetDays int) (int, error) {
|
|
if _, err := p.client.Channel.Get(channelID); err != nil {
|
|
return 0, pkgerrors.Wrap(err, "failed to get channel")
|
|
}
|
|
|
|
cutoff := time.Now().AddDate(0, 0, -offsetDays).UnixMilli()
|
|
return deleteChannelPosts(&p.client.Post, channelID, cutoff, &p.client.Log)
|
|
}
|
|
|
|
func cutoffMillis(offsetDays int, now time.Time) int64 {
|
|
return now.AddDate(0, 0, -offsetDays).UnixMilli()
|
|
}
|