mattermost-community-enterp.../channels/app/post_helpers.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

266 lines
9.0 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"sort"
"github.com/mattermost/mattermost/server/public/model"
)
type filterPostOptions struct {
assumeSortedCreatedAt bool
}
type accessibleBounds struct {
start int
end int
}
func (b accessibleBounds) allAccessible(lenPosts int) bool {
return b.start == allAccessibleBounds(lenPosts).start && b.end == allAccessibleBounds(lenPosts).end
}
func (b accessibleBounds) noAccessible() bool {
return b.start == noAccessibleBounds.start && b.end == noAccessibleBounds.end
}
// assumes checking was already performed that at least one post is inaccessible
func (b accessibleBounds) getInaccessibleRange(listLength int) (int, int) {
var start, end int
if b.start == 0 {
start = b.end + 1
end = listLength - 1
} else {
start = 0
end = b.start - 1
}
return start, end
}
var noAccessibleBounds = accessibleBounds{start: -1, end: -1}
var allAccessibleBounds = func(lenPosts int) accessibleBounds { return accessibleBounds{start: 0, end: lenPosts - 1} }
// getTimeSortedPostAccessibleBounds returns what the boundaries are for accessible posts.
// It assumes that CreateAt time for posts is monotonically increasing or decreasing.
// It could be either because posts can be returned in ascending or descending time order.
// Special values (which can be checked with methods `allAccessible` and `allInaccessible`)
// denote if all or none of the posts are accessible.
func getTimeSortedPostAccessibleBounds(earliestAccessibleTime int64, lenPosts int, getCreateAt func(int) int64) accessibleBounds {
if lenPosts == 0 {
return allAccessibleBounds(lenPosts)
}
if lenPosts == 1 {
if getCreateAt(0) >= earliestAccessibleTime {
return allAccessibleBounds(lenPosts)
}
return noAccessibleBounds
}
ascending := getCreateAt(0) < getCreateAt(lenPosts-1)
idx := sort.Search(lenPosts, func(i int) bool {
if ascending {
// Ascending order automatically picks the left most post(at idx),
// in case multiple posts at idx, idx+1, idx+2... have the same time.
return getCreateAt(i) >= earliestAccessibleTime
}
// Special case(subtracting 1) for descending order to include the right most post(at idx+k),
// in case multiple posts at idx, idx+1, idx+2...idx+k have the same time.
return getCreateAt(i) <= earliestAccessibleTime-1
})
if ascending {
if idx == lenPosts {
return noAccessibleBounds
}
return accessibleBounds{start: idx, end: lenPosts - 1}
}
if idx == 0 {
return noAccessibleBounds
}
return accessibleBounds{start: 0, end: idx - 1}
}
// linearFilterPostList make no assumptions about ordering, go through posts one by one
// this is the slower fallback that is still safe if we can not
// assume posts are ordered by CreatedAt
func linearFilterPostList(postList *model.PostList, earliestAccessibleTime int64) {
// filter Posts
posts := postList.Posts
order := postList.Order
n := 0
for i, postID := range order {
if createAt := posts[postID].CreateAt; createAt >= earliestAccessibleTime {
order[n] = order[i]
n++
} else {
if createAt > postList.FirstInaccessiblePostTime {
postList.FirstInaccessiblePostTime = createAt
}
delete(posts, postID)
}
}
postList.Order = order[:n]
// it can happen that some post list results don't have all posts in the Order field.
// for example GetPosts in the CollapsedThreads = false path, parents are not added
// to Order
for postId := range posts {
if createAt := posts[postId].CreateAt; createAt < earliestAccessibleTime {
if createAt > postList.FirstInaccessiblePostTime {
postList.FirstInaccessiblePostTime = createAt
}
delete(posts, postId)
}
}
}
// linearFilterPostsSlice make no assumptions about ordering, go through posts one by one
// this is the slower fallback that is still safe if we can not
// assume posts are ordered by CreatedAt
func linearFilterPostsSlice(posts []*model.Post, earliestAccessibleTime int64) ([]*model.Post, int64) {
var firstInaccessiblePostTime int64
n := 0
for i := range posts {
if createAt := posts[i].CreateAt; createAt >= earliestAccessibleTime {
posts[n] = posts[i]
n++
} else {
if createAt > firstInaccessiblePostTime {
firstInaccessiblePostTime = createAt
}
}
}
return posts[:n], firstInaccessiblePostTime
}
// filterInaccessiblePosts filters out the posts, past the cloud limit
func (a *App) filterInaccessiblePosts(postList *model.PostList, options filterPostOptions) *model.AppError {
if postList == nil || postList.Posts == nil || len(postList.Posts) == 0 {
return nil
}
lastAccessiblePostTime, appErr := a.GetLastAccessiblePostTime()
if appErr != nil {
return model.NewAppError("filterInaccessiblePosts", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
if lastAccessiblePostTime == 0 {
// No need to filter, all posts are accessible
return nil
}
if len(postList.Posts) == len(postList.Order) && options.assumeSortedCreatedAt {
lenPosts := len(postList.Posts)
getCreateAt := func(i int) int64 { return postList.Posts[postList.Order[i]].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessiblePostTime, lenPosts, getCreateAt)
if bounds.allAccessible(lenPosts) {
return nil
}
if bounds.noAccessible() {
if lenPosts > 0 {
firstPostCreatedAt := postList.Posts[postList.Order[0]].CreateAt
lastPostCreatedAt := postList.Posts[postList.Order[len(postList.Order)-1]].CreateAt
postList.FirstInaccessiblePostTime = max(firstPostCreatedAt, lastPostCreatedAt)
}
postList.Posts = map[string]*model.Post{}
postList.Order = []string{}
return nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(postList.Order))
startInaccessibleCreatedAt := postList.Posts[postList.Order[startInaccessibleIndex]].CreateAt
endInaccessibleCreatedAt := postList.Posts[postList.Order[endInaccessibleIndex]].CreateAt
postList.FirstInaccessiblePostTime = max(startInaccessibleCreatedAt, endInaccessibleCreatedAt)
posts := postList.Posts
order := postList.Order
accessibleCount := bounds.end - bounds.start + 1
inaccessibleCount := lenPosts - accessibleCount
// Linearly cover shorter route to traverse posts map
if inaccessibleCount < accessibleCount {
for i := 0; i < bounds.start; i++ {
delete(posts, order[i])
}
for i := bounds.end + 1; i < lenPosts; i++ {
delete(posts, order[i])
}
} else {
accessiblePosts := make(map[string]*model.Post, accessibleCount)
for i := bounds.start; i <= bounds.end; i++ {
accessiblePosts[order[i]] = posts[order[i]]
}
postList.Posts = accessiblePosts
}
postList.Order = postList.Order[bounds.start : bounds.end+1]
} else {
linearFilterPostList(postList, lastAccessiblePostTime)
}
return nil
}
// isInaccessiblePost indicates if the post is past the cloud plan's limit.
func (a *App) isInaccessiblePost(post *model.Post) (int64, *model.AppError) {
if post == nil {
return 0, nil
}
pl := &model.PostList{
Order: []string{post.Id},
Posts: map[string]*model.Post{post.Id: post},
}
return pl.FirstInaccessiblePostTime, a.filterInaccessiblePosts(pl, filterPostOptions{assumeSortedCreatedAt: true})
}
// getFilteredAccessiblePosts returns accessible posts filtered as per the cloud plan's limit and also indicates if there were any inaccessible posts
func (a *App) getFilteredAccessiblePosts(posts []*model.Post, options filterPostOptions) ([]*model.Post, int64, *model.AppError) {
if len(posts) == 0 {
return posts, 0, nil
}
filteredPosts := []*model.Post{}
lastAccessiblePostTime, appErr := a.GetLastAccessiblePostTime()
if appErr != nil {
return filteredPosts, 0, model.NewAppError("getFilteredAccessiblePosts", "app.last_accessible_post.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
} else if lastAccessiblePostTime == 0 {
// No need to filter, all posts are accessible
return posts, 0, nil
}
if options.assumeSortedCreatedAt {
lenPosts := len(posts)
getCreateAt := func(i int) int64 { return posts[i].CreateAt }
bounds := getTimeSortedPostAccessibleBounds(lastAccessiblePostTime, lenPosts, getCreateAt)
if bounds.allAccessible(lenPosts) {
return posts, 0, nil
}
if bounds.noAccessible() {
var firstInaccessiblePostTime int64
if lenPosts > 0 {
firstPostCreatedAt := posts[0].CreateAt
lastPostCreatedAt := posts[len(posts)-1].CreateAt
firstInaccessiblePostTime = max(firstPostCreatedAt, lastPostCreatedAt)
}
return filteredPosts, firstInaccessiblePostTime, nil
}
startInaccessibleIndex, endInaccessibleIndex := bounds.getInaccessibleRange(len(posts))
firstPostCreatedAt := posts[startInaccessibleIndex].CreateAt
lastPostCreatedAt := posts[endInaccessibleIndex].CreateAt
firstInaccessiblePostTime := max(firstPostCreatedAt, lastPostCreatedAt)
filteredPosts = posts[bounds.start : bounds.end+1]
return filteredPosts, firstInaccessiblePostTime, nil
}
filteredPosts, firstInaccessiblePostTime := linearFilterPostsSlice(posts, lastAccessiblePostTime)
return filteredPosts, firstInaccessiblePostTime, nil
}