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>
1420 lines
43 KiB
Go
1420 lines
43 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app"
|
|
"github.com/mattermost/mattermost/server/v8/channels/web"
|
|
)
|
|
|
|
func (api *API) InitPost() {
|
|
api.BaseRoutes.Posts.Handle("", api.APISessionRequired(createPost)).Methods(http.MethodPost)
|
|
api.BaseRoutes.Post.Handle("", api.APISessionRequired(getPost)).Methods(http.MethodGet)
|
|
api.BaseRoutes.Post.Handle("", api.APISessionRequired(deletePost)).Methods(http.MethodDelete)
|
|
api.BaseRoutes.Posts.Handle("/ids", api.APISessionRequired(getPostsByIds)).Methods(http.MethodPost)
|
|
api.BaseRoutes.Posts.Handle("/ephemeral", api.APISessionRequired(createEphemeralPost)).Methods(http.MethodPost)
|
|
api.BaseRoutes.Post.Handle("/edit_history", api.APISessionRequired(getEditHistoryForPost)).Methods(http.MethodGet)
|
|
api.BaseRoutes.Post.Handle("/thread", api.APISessionRequired(getPostThread)).Methods(http.MethodGet)
|
|
api.BaseRoutes.Post.Handle("/info", api.APISessionRequired(getPostInfo)).Methods(http.MethodGet)
|
|
api.BaseRoutes.Post.Handle("/files/info", api.APISessionRequired(getFileInfosForPost)).Methods(http.MethodGet)
|
|
api.BaseRoutes.PostsForChannel.Handle("", api.APISessionRequired(getPostsForChannel)).Methods(http.MethodGet)
|
|
api.BaseRoutes.PostsForUser.Handle("/flagged", api.APISessionRequired(getFlaggedPostsForUser)).Methods(http.MethodGet)
|
|
|
|
api.BaseRoutes.ChannelForUser.Handle("/posts/unread", api.APISessionRequired(getPostsForChannelAroundLastUnread)).Methods(http.MethodGet)
|
|
|
|
api.BaseRoutes.Team.Handle("/posts/search", api.APISessionRequiredDisableWhenBusy(searchPostsInTeam)).Methods(http.MethodPost)
|
|
api.BaseRoutes.Posts.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchPostsInAllTeams)).Methods(http.MethodPost)
|
|
api.BaseRoutes.Post.Handle("", api.APISessionRequired(updatePost)).Methods(http.MethodPut)
|
|
api.BaseRoutes.Post.Handle("/patch", api.APISessionRequired(patchPost)).Methods(http.MethodPut)
|
|
api.BaseRoutes.Post.Handle("/restore/{restore_version_id:[A-Za-z0-9]+}", api.APISessionRequired(restorePostVersion)).Methods(http.MethodPost)
|
|
api.BaseRoutes.PostForUser.Handle("/set_unread", api.APISessionRequired(setPostUnread)).Methods(http.MethodPost)
|
|
api.BaseRoutes.PostForUser.Handle("/reminder", api.APISessionRequired(setPostReminder)).Methods(http.MethodPost)
|
|
|
|
api.BaseRoutes.Post.Handle("/pin", api.APISessionRequired(pinPost)).Methods(http.MethodPost)
|
|
api.BaseRoutes.Post.Handle("/unpin", api.APISessionRequired(unpinPost)).Methods(http.MethodPost)
|
|
|
|
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(acknowledgePost)).Methods(http.MethodPost)
|
|
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(unacknowledgePost)).Methods(http.MethodDelete)
|
|
|
|
api.BaseRoutes.Post.Handle("/move", api.APISessionRequired(moveThread)).Methods(http.MethodPost)
|
|
|
|
api.BaseRoutes.Posts.Handle("/rewrite", api.APISessionRequired(rewriteMessage)).Methods(http.MethodPost)
|
|
}
|
|
|
|
func createPostChecks(where string, c *Context, post *model.Post) {
|
|
// ***************************************************************
|
|
// NOTE - if you make any change here, please make sure to apply the
|
|
// same change for scheduled posts as well in the `scheduledPostChecks()` function
|
|
// in API layer.
|
|
// ***************************************************************
|
|
|
|
userCreatePostPermissionCheckWithContext(c, post.ChannelId)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
postHardenedModeCheckWithContext(where, c, post.GetProps())
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
postPriorityCheckWithContext(where, c, post.GetPriority(), post.RootId)
|
|
}
|
|
|
|
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
var post model.Post
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
|
|
c.SetInvalidParamWithErr("post", jsonErr)
|
|
return
|
|
}
|
|
|
|
post.SanitizeInput()
|
|
post.UserId = c.AppContext.Session().UserId
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventCreatePost, model.AuditStatusFail)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "post", &post)
|
|
|
|
if post.CreateAt != 0 && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
post.CreateAt = 0
|
|
}
|
|
|
|
createPostChecks("Api4.createPost", c, &post)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
setOnline := r.URL.Query().Get("set_online")
|
|
setOnlineBool := true // By default, always set online.
|
|
var err2 error
|
|
if setOnline != "" {
|
|
setOnlineBool, err2 = strconv.ParseBool(setOnline)
|
|
if err2 != nil {
|
|
c.Logger.Warn("Failed to parse set_online URL query parameter from createPost request", mlog.Err(err2))
|
|
setOnlineBool = true // Set online nevertheless.
|
|
}
|
|
}
|
|
|
|
rp, err := c.App.CreatePostAsUser(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), c.AppContext.Session().Id, setOnlineBool)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
auditRec.Success()
|
|
auditRec.AddEventResultState(rp)
|
|
auditRec.AddEventObjectType("post")
|
|
|
|
if setOnlineBool {
|
|
c.App.SetStatusOnline(c.AppContext.Session().UserId, false)
|
|
}
|
|
|
|
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
|
|
c.ExtendSessionExpiryIfNeeded(w, r)
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
// Note that rp has already had PreparePostForClient called on it by App.CreatePost
|
|
if err := rp.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func createEphemeralPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
ephRequest := model.PostEphemeral{}
|
|
|
|
jsonErr := json.NewDecoder(r.Body).Decode(&ephRequest)
|
|
if jsonErr != nil {
|
|
c.SetInvalidParamWithErr("body", jsonErr)
|
|
return
|
|
}
|
|
|
|
if ephRequest.UserID == "" {
|
|
c.SetInvalidParam("user_id")
|
|
return
|
|
}
|
|
|
|
if ephRequest.Post == nil {
|
|
c.SetInvalidParam("post")
|
|
return
|
|
}
|
|
|
|
ephRequest.Post.UserId = c.AppContext.Session().UserId
|
|
ephRequest.Post.CreateAt = model.GetMillis()
|
|
|
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreatePostEphemeral) {
|
|
c.SetPermissionError(model.PermissionCreatePostEphemeral)
|
|
return
|
|
}
|
|
|
|
rp := c.App.SendEphemeralPost(c.AppContext, ephRequest.UserID, c.App.PostWithProxyRemovedFromImageURLs(ephRequest.Post))
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
rp = model.AddPostActionCookies(rp, c.App.PostActionCookieSecret())
|
|
rp = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, rp, &model.PreparePostForClientOpts{IsNewPost: true, IncludePriority: true})
|
|
rp, err := c.App.SanitizePostMetadataForUser(c.AppContext, rp, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if err := rp.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireChannelId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
afterPost := r.URL.Query().Get("after")
|
|
if afterPost != "" && !model.IsValidId(afterPost) {
|
|
c.SetInvalidParam("after")
|
|
return
|
|
}
|
|
|
|
beforePost := r.URL.Query().Get("before")
|
|
if beforePost != "" && !model.IsValidId(beforePost) {
|
|
c.SetInvalidParam("before")
|
|
return
|
|
}
|
|
|
|
sinceString := r.URL.Query().Get("since")
|
|
var since int64
|
|
var parseError error
|
|
if sinceString != "" {
|
|
since, parseError = strconv.ParseInt(sinceString, 10, 64)
|
|
if parseError != nil {
|
|
c.SetInvalidParamWithErr("since", parseError)
|
|
return
|
|
}
|
|
}
|
|
skipFetchThreads, _ := strconv.ParseBool(r.URL.Query().Get("skipFetchThreads"))
|
|
collapsedThreads, _ := strconv.ParseBool(r.URL.Query().Get("collapsedThreads"))
|
|
collapsedThreadsExtended, _ := strconv.ParseBool(r.URL.Query().Get("collapsedThreadsExtended"))
|
|
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
|
|
channelId := c.Params.ChannelId
|
|
page := c.Params.Page
|
|
perPage := c.Params.PerPage
|
|
|
|
if !c.IsSystemAdmin() && includeDeleted {
|
|
c.SetPermissionError(model.PermissionReadDeletedPosts)
|
|
return
|
|
}
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, channelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
var list *model.PostList
|
|
etag := ""
|
|
|
|
if since > 0 {
|
|
list, err = c.App.GetPostsSince(c.AppContext, model.GetPostsSinceOptions{ChannelId: channelId, Time: since, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId})
|
|
} else if afterPost != "" {
|
|
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
|
|
|
|
if c.HandleEtag(etag, "Get Posts After", w, r) {
|
|
return
|
|
}
|
|
|
|
list, err = c.App.GetPostsAfterPost(c.AppContext, model.GetPostsOptions{ChannelId: channelId, PostId: afterPost, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
|
|
} else if beforePost != "" {
|
|
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
|
|
|
|
if c.HandleEtag(etag, "Get Posts Before", w, r) {
|
|
return
|
|
}
|
|
|
|
list, err = c.App.GetPostsBeforePost(c.AppContext, model.GetPostsOptions{ChannelId: channelId, PostId: beforePost, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
|
|
} else {
|
|
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
|
|
|
|
if c.HandleEtag(etag, "Get Posts", w, r) {
|
|
return
|
|
}
|
|
|
|
list, err = c.App.GetPostsPage(c.AppContext, model.GetPostsOptions{ChannelId: channelId, Page: page, PerPage: perPage, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId, IncludeDeleted: includeDeleted})
|
|
}
|
|
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if etag != "" {
|
|
w.Header().Set(model.HeaderEtagServer, etag)
|
|
}
|
|
|
|
c.App.AddCursorIdsForPostList(list, afterPost, beforePost, since, page, perPage, collapsedThreads)
|
|
clientPostList := c.App.PreparePostListForClient(c.AppContext, list)
|
|
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if err := clientPostList.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireUserId().RequireChannelId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
userId := c.Params.UserId
|
|
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userId) {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
return
|
|
}
|
|
|
|
channelId := c.Params.ChannelId
|
|
channel, err := c.App.GetChannel(c.AppContext, channelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
if c.Params.LimitAfter == 0 {
|
|
c.SetInvalidURLParam("limit_after")
|
|
return
|
|
}
|
|
|
|
skipFetchThreads := r.URL.Query().Get("skipFetchThreads") == "true"
|
|
collapsedThreads := r.URL.Query().Get("collapsedThreads") == "true"
|
|
collapsedThreadsExtended := r.URL.Query().Get("collapsedThreadsExtended") == "true"
|
|
|
|
postList, err := c.App.GetPostsForChannelAroundLastUnread(c.AppContext, channelId, userId, c.Params.LimitBefore, c.Params.LimitAfter, skipFetchThreads, collapsedThreads, collapsedThreadsExtended)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
etag := ""
|
|
if len(postList.Order) == 0 {
|
|
etag = c.App.GetPostsEtag(channelId, collapsedThreads)
|
|
|
|
if c.HandleEtag(etag, "Get Posts", w, r) {
|
|
return
|
|
}
|
|
|
|
postList, err = c.App.GetPostsPage(c.AppContext, model.GetPostsOptions{ChannelId: channelId, Page: app.PageDefault, PerPage: c.Params.LimitBefore, SkipFetchThreads: skipFetchThreads, CollapsedThreads: collapsedThreads, CollapsedThreadsExtended: collapsedThreadsExtended, UserId: c.AppContext.Session().UserId})
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
postList.NextPostId = c.App.GetNextPostIdFromPostList(postList, collapsedThreads)
|
|
postList.PrevPostId = c.App.GetPrevPostIdFromPostList(postList, collapsedThreads)
|
|
|
|
clientPostList := c.App.PreparePostListForClient(c.AppContext, postList)
|
|
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if etag != "" {
|
|
w.Header().Set(model.HeaderEtagServer, etag)
|
|
}
|
|
if err := clientPostList.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
return
|
|
}
|
|
|
|
channelId := r.URL.Query().Get("channel_id")
|
|
teamId := r.URL.Query().Get("team_id")
|
|
|
|
var posts *model.PostList
|
|
var err *model.AppError
|
|
|
|
if channelId != "" {
|
|
posts, err = c.App.GetFlaggedPostsForChannel(c.Params.UserId, channelId, c.Params.Page, c.Params.PerPage)
|
|
} else if teamId != "" {
|
|
posts, err = c.App.GetFlaggedPostsForTeam(c.Params.UserId, teamId, c.Params.Page, c.Params.PerPage)
|
|
} else {
|
|
posts, err = c.App.GetFlaggedPosts(c.Params.UserId, c.Params.Page, c.Params.PerPage)
|
|
}
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
channelMap := make(map[string]*model.Channel)
|
|
channelIds := []string{}
|
|
for _, post := range posts.Posts {
|
|
channelIds = append(channelIds, post.ChannelId)
|
|
}
|
|
channels, err := c.App.GetChannels(c.AppContext, channelIds)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
for _, channel := range channels {
|
|
channelMap[channel.Id] = channel
|
|
}
|
|
|
|
pl := model.NewPostList()
|
|
channelReadPermission := make(map[string]bool)
|
|
|
|
for _, post := range posts.Posts {
|
|
allowed, ok := channelReadPermission[post.ChannelId]
|
|
|
|
if !ok {
|
|
allowed = false
|
|
|
|
channel, ok := channelMap[post.ChannelId]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
|
allowed = true
|
|
}
|
|
|
|
channelReadPermission[post.ChannelId] = allowed
|
|
}
|
|
|
|
if !allowed {
|
|
continue
|
|
}
|
|
|
|
pl.AddPost(post)
|
|
pl.AddOrder(post.Id)
|
|
}
|
|
|
|
pl.SortByCreateAt()
|
|
clientPostList := c.App.PreparePostListForClient(c.AppContext, pl)
|
|
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if err := clientPostList.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
// getPost also sets a header to indicate, if post is inaccessible due to the cloud plan's limit.
|
|
func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
|
|
if includeDeleted && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
c.SetPermissionError(model.PermissionManageSystem)
|
|
return
|
|
}
|
|
|
|
post, err := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), includeDeleted)
|
|
if err != nil {
|
|
c.Err = err
|
|
|
|
// Post is inaccessible due to cloud plan's limit.
|
|
if err.Id == "app.post.cloud.get.app_error" {
|
|
w.Header().Set(model.HeaderFirstInaccessiblePostTime, "1")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, &model.PreparePostForClientOpts{IncludePriority: true})
|
|
post, err = c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if c.HandleEtag(post.Etag(), "Get Post", w, r) {
|
|
return
|
|
}
|
|
|
|
w.Header().Set(model.HeaderEtagServer, post.Etag())
|
|
if err := post.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
// getPostsByIds also sets a header to indicate, if posts were truncated as per the cloud plan's limit.
|
|
func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
postIDs, err := model.SortedArrayFromJSON(r.Body)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("getPostsByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
|
|
return
|
|
} else if len(postIDs) == 0 {
|
|
c.SetInvalidParam("post_ids")
|
|
return
|
|
}
|
|
|
|
if len(postIDs) > 1000 {
|
|
c.Err = model.NewAppError("getPostsByIds", "api.post.posts_by_ids.invalid_body.request_error", map[string]any{"MaxLength": 1000}, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
postsList, firstInaccessiblePostTime, appErr := c.App.GetPostsByIds(postIDs)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
channelMap := make(map[string]*model.Channel)
|
|
channelIds := []string{}
|
|
for _, post := range postsList {
|
|
channelIds = append(channelIds, post.ChannelId)
|
|
}
|
|
channels, appErr := c.App.GetChannels(c.AppContext, channelIds)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
for _, channel := range channels {
|
|
channelMap[channel.Id] = channel
|
|
}
|
|
|
|
var posts = []*model.Post{}
|
|
for _, post := range postsList {
|
|
channel, ok := channelMap[post.ChannelId]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
|
continue
|
|
}
|
|
|
|
post = c.App.PreparePostForClient(c.AppContext, post, &model.PreparePostForClientOpts{IncludePriority: true})
|
|
post.StripActionIntegrations()
|
|
posts = append(posts, post)
|
|
}
|
|
|
|
w.Header().Set(model.HeaderFirstInaccessiblePostTime, strconv.FormatInt(firstInaccessiblePostTime, 10))
|
|
|
|
if err := json.NewEncoder(w).Encode(posts); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getEditHistoryForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
originalPost, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
|
if err != nil {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditPost) {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
|
|
if c.AppContext.Session().UserId != originalPost.UserId {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
|
|
postsList, err := c.App.GetEditHistoryForPost(c.Params.PostId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if err := json.NewEncoder(w).Encode(postsList); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func deletePost(c *Context, w http.ResponseWriter, _ *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
permanent := c.Params.Permanent
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventDeletePost, model.AuditStatusFail)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
model.AddEventParameterToAuditRec(auditRec, "post_id", c.Params.PostId)
|
|
model.AddEventParameterToAuditRec(auditRec, "permanent", permanent)
|
|
|
|
includeDeleted := permanent
|
|
|
|
if permanent && !*c.App.Config().ServiceSettings.EnableAPIPostDeletion {
|
|
c.Err = model.NewAppError("deletePost", "api.post.delete_post.not_enabled.app_error", nil, "postId="+c.Params.PostId, http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
if permanent && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
c.SetPermissionError(model.PermissionManageSystem)
|
|
return
|
|
}
|
|
|
|
post, appErr := c.App.GetSinglePost(c.AppContext, c.Params.PostId, includeDeleted)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
auditRec.AddEventPriorState(post)
|
|
auditRec.AddEventObjectType("post")
|
|
|
|
if c.AppContext.Session().UserId == post.UserId {
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeletePost) {
|
|
c.SetPermissionError(model.PermissionDeletePost)
|
|
return
|
|
}
|
|
} else {
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeleteOthersPosts) {
|
|
c.SetPermissionError(model.PermissionDeleteOthersPosts)
|
|
return
|
|
}
|
|
}
|
|
|
|
if permanent {
|
|
appErr = c.App.PermanentDeletePost(c.AppContext, c.Params.PostId, c.AppContext.Session().UserId)
|
|
} else {
|
|
_, appErr = c.App.DeletePost(c.AppContext, c.Params.PostId, c.AppContext.Session().UserId)
|
|
}
|
|
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// For now, by default we return all items unless it's set to maintain
|
|
// backwards compatibility with mobile. But when the next ESR passes, we need to
|
|
// change this to web.PerPageDefault.
|
|
perPage := 0
|
|
if perPageStr := r.URL.Query().Get("perPage"); perPageStr != "" {
|
|
var err error
|
|
perPage, err = strconv.Atoi(perPageStr)
|
|
if err != nil || perPage > web.PerPageMaximum {
|
|
c.SetInvalidParamWithErr("perPage", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
var fromCreateAt int64
|
|
if fromCreateAtStr := r.URL.Query().Get("fromCreateAt"); fromCreateAtStr != "" {
|
|
var err error
|
|
fromCreateAt, err = strconv.ParseInt(fromCreateAtStr, 10, 64)
|
|
if err != nil {
|
|
c.SetInvalidParamWithErr("fromCreateAt", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
fromPost := r.URL.Query().Get("fromPost")
|
|
// Either only fromCreateAt must be set, or both fromPost and fromCreateAt must be set
|
|
if fromPost != "" && fromCreateAt == 0 {
|
|
c.SetInvalidParam("if fromPost is set, then fromCreateAt must also be set")
|
|
return
|
|
}
|
|
|
|
var fromUpdateAt int64
|
|
if fromUpdateAtStr := r.URL.Query().Get("fromUpdateAt"); fromUpdateAtStr != "" {
|
|
var err error
|
|
fromUpdateAt, err = strconv.ParseInt(fromUpdateAtStr, 10, 64)
|
|
if err != nil {
|
|
c.SetInvalidParamWithErr("fromUpdateAt", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
if fromUpdateAt != 0 && fromCreateAt != 0 {
|
|
c.SetInvalidParamWithDetails("fromUpdateAt", "both fromUpdateAt and fromCreateAt cannot be set")
|
|
return
|
|
}
|
|
|
|
updatesOnly := r.URL.Query().Get("updatesOnly") == "true"
|
|
if updatesOnly && fromUpdateAt == 0 {
|
|
c.SetInvalidParamWithDetails("fromUpdateAt", "fromUpdateAt must be set if updatesOnly is set")
|
|
return
|
|
}
|
|
|
|
direction := ""
|
|
if dir := r.URL.Query().Get("direction"); dir != "" {
|
|
if dir != "up" && dir != "down" {
|
|
c.SetInvalidParam("direction")
|
|
return
|
|
}
|
|
direction = dir
|
|
}
|
|
|
|
if updatesOnly && direction == "up" {
|
|
c.SetInvalidParamWithDetails("updatesOnly", "updatesOnly flag cannot be used with up direction")
|
|
return
|
|
}
|
|
|
|
opts := model.GetPostsOptions{
|
|
SkipFetchThreads: r.URL.Query().Get("skipFetchThreads") == "true",
|
|
CollapsedThreads: r.URL.Query().Get("collapsedThreads") == "true",
|
|
CollapsedThreadsExtended: r.URL.Query().Get("collapsedThreadsExtended") == "true",
|
|
UpdatesOnly: updatesOnly,
|
|
PerPage: perPage,
|
|
Direction: direction,
|
|
FromPost: fromPost,
|
|
FromCreateAt: fromCreateAt,
|
|
FromUpdateAt: fromUpdateAt,
|
|
}
|
|
list, err := c.App.GetPostThread(c.AppContext, c.Params.PostId, opts, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if list.FirstInaccessiblePostTime != 0 {
|
|
// e.g. if root post is archived in a cloud plan,
|
|
// we don't want to display the thread,
|
|
// but at the same time the request was not bad,
|
|
// so we return the time of archival and let the client
|
|
// show an error
|
|
if err := (&model.PostList{Order: []string{}, FirstInaccessiblePostTime: list.FirstInaccessiblePostTime}).EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
return
|
|
}
|
|
|
|
post, ok := list.Posts[c.Params.PostId]
|
|
if !ok {
|
|
c.SetInvalidURLParam("post_id")
|
|
return
|
|
}
|
|
|
|
if _, err = c.App.GetPostIfAuthorized(c.AppContext, post.Id, c.AppContext.Session(), false); err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
if c.HandleEtag(list.Etag(), "Get Post Thread", w, r) {
|
|
return
|
|
}
|
|
|
|
clientPostList := c.App.PreparePostListForClient(c.AppContext, list)
|
|
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
w.Header().Set(model.HeaderEtagServer, clientPostList.Etag())
|
|
|
|
if err := clientPostList.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func searchPostsInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireTeamId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
|
c.SetPermissionError(model.PermissionViewTeam)
|
|
return
|
|
}
|
|
|
|
searchPosts(c, w, r, c.Params.TeamId)
|
|
}
|
|
|
|
func searchPostsInAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
searchPosts(c, w, r, "")
|
|
}
|
|
|
|
func searchPosts(c *Context, w http.ResponseWriter, r *http.Request, teamId string) {
|
|
var params model.SearchParameter
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(¶ms); jsonErr != nil {
|
|
c.Err = model.NewAppError("searchPosts", "api.post.search_posts.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
|
|
return
|
|
}
|
|
|
|
if params.Terms == nil || *params.Terms == "" {
|
|
c.SetInvalidParam("terms")
|
|
return
|
|
}
|
|
terms := *params.Terms
|
|
|
|
timeZoneOffset := 0
|
|
if params.TimeZoneOffset != nil {
|
|
timeZoneOffset = *params.TimeZoneOffset
|
|
}
|
|
|
|
isOrSearch := false
|
|
if params.IsOrSearch != nil {
|
|
isOrSearch = *params.IsOrSearch
|
|
}
|
|
|
|
page := 0
|
|
if params.Page != nil {
|
|
page = *params.Page
|
|
}
|
|
|
|
perPage := 60
|
|
if params.PerPage != nil {
|
|
perPage = *params.PerPage
|
|
}
|
|
|
|
includeDeletedChannels := false
|
|
if params.IncludeDeletedChannels != nil {
|
|
includeDeletedChannels = *params.IncludeDeletedChannels
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventSearchPosts, model.AuditStatusFail)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelAPI)
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "search_params", params)
|
|
|
|
startTime := time.Now()
|
|
|
|
results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
|
|
|
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
|
metrics := c.App.Metrics()
|
|
if metrics != nil {
|
|
metrics.IncrementPostsSearchCounter()
|
|
metrics.ObservePostsSearchDuration(elapsedTime)
|
|
}
|
|
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
clientPostList := c.App.PreparePostListForClient(c.AppContext, results.PostList)
|
|
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
results = model.MakePostSearchResults(clientPostList, results.Matches)
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "search_results", results)
|
|
auditRec.Success()
|
|
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
if err := results.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
var post model.Post
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
|
|
c.SetInvalidParamWithErr("post", jsonErr)
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventUpdatePost, model.AuditStatusFail)
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "post", &post)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
|
|
// The post being updated in the payload must be the same one as indicated in the URL.
|
|
if post.Id != c.Params.PostId {
|
|
c.SetInvalidParam("id")
|
|
return
|
|
}
|
|
|
|
postHardenedModeCheckWithContext("UpdatePost", c, post.GetProps())
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
originalPost, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
|
if err != nil {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditPost) {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
|
|
auditRec.AddEventPriorState(originalPost)
|
|
auditRec.AddEventObjectType("post")
|
|
|
|
// passing a nil fileIds should not have any effect on a post's file IDs
|
|
// so, we restore the original file IDs in this case
|
|
if post.FileIds == nil {
|
|
post.FileIds = originalPost.FileIds
|
|
}
|
|
|
|
if c.AppContext.Session().UserId != originalPost.UserId {
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditOthersPosts) {
|
|
c.SetPermissionError(model.PermissionEditOthersPosts)
|
|
return
|
|
}
|
|
}
|
|
|
|
post.Id = c.Params.PostId
|
|
|
|
if *c.App.Config().ServiceSettings.PostEditTimeLimit != -1 && model.GetMillis() > originalPost.CreateAt+int64(*c.App.Config().ServiceSettings.PostEditTimeLimit*1000) && post.Message != originalPost.Message {
|
|
c.Err = model.NewAppError("UpdatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]any{"timeLimit": *c.App.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
rpost, err := c.App.UpdatePost(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), &model.UpdatePostOptions{SafeUpdate: false})
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
auditRec.AddEventResultState(rpost)
|
|
|
|
if err := rpost.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
var post model.PostPatch
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil {
|
|
c.SetInvalidParamWithErr("post", jsonErr)
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventPatchPost, model.AuditStatusFail)
|
|
model.AddEventParameterToAuditRec(auditRec, "id", c.Params.PostId)
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "patch", &post)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
|
|
if post.Props != nil {
|
|
postHardenedModeCheckWithContext("patchPost", c, *post.Props)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
postPatchChecks(c, auditRec, post.Message)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, c.App.PostPatchWithProxyRemovedFromImageURLs(&post), nil)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
auditRec.AddEventResultState(patchedPost)
|
|
|
|
if err := patchedPost.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func postPatchChecks(c *Context, auditRec *model.AuditRecord, message *string) {
|
|
originalPost, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
|
if err != nil {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
auditRec.AddEventPriorState(originalPost)
|
|
auditRec.AddEventObjectType("post")
|
|
|
|
var permission *model.Permission
|
|
|
|
if c.AppContext.Session().UserId == originalPost.UserId {
|
|
permission = model.PermissionEditPost
|
|
} else {
|
|
permission = model.PermissionEditOthersPosts
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, permission) {
|
|
c.SetPermissionError(permission)
|
|
return
|
|
}
|
|
|
|
if *c.App.Config().ServiceSettings.PostEditTimeLimit != -1 && model.GetMillis() > originalPost.CreateAt+int64(*c.App.Config().ServiceSettings.PostEditTimeLimit*1000) && message != nil {
|
|
c.Err = model.NewAppError("patchPost", "api.post.update_post.permissions_time_limit.app_error", map[string]any{"timeLimit": *c.App.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
func setPostUnread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId().RequireUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
props := model.MapBoolFromJSON(r.Body)
|
|
collapsedThreadsSupported := props["collapsed_threads_supported"]
|
|
|
|
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
return
|
|
}
|
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
state, err := c.App.MarkChannelAsUnreadFromPost(c.AppContext, c.Params.PostId, c.Params.UserId, collapsedThreadsSupported)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if err := json.NewEncoder(w).Encode(state); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func setPostReminder(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId().RequireUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if c.AppContext.Session().UserId != c.Params.UserId && !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
return
|
|
}
|
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
var reminder model.PostReminder
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&reminder); jsonErr != nil {
|
|
c.SetInvalidParamWithErr("target_time", jsonErr)
|
|
return
|
|
}
|
|
|
|
appErr := c.App.SetPostReminder(c.AppContext, c.Params.PostId, c.Params.UserId, reminder.TargetTime)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func saveIsPinnedPost(c *Context, w http.ResponseWriter, isPinned bool) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventSaveIsPinnedPost, model.AuditStatusFail)
|
|
model.AddEventParameterToAuditRec(auditRec, "post_id", c.Params.PostId)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
|
|
post, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
|
if err != nil {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
auditRec.AddEventPriorState(post)
|
|
auditRec.AddEventObjectType("post")
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, post.ChannelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
patch := &model.PostPatch{}
|
|
patch.IsPinned = model.NewPointer(isPinned)
|
|
|
|
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, patch, nil)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
auditRec.AddEventResultState(patchedPost)
|
|
|
|
auditRec.Success()
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func pinPost(c *Context, w http.ResponseWriter, _ *http.Request) {
|
|
saveIsPinnedPost(c, w, true)
|
|
}
|
|
|
|
func unpinPost(c *Context, w http.ResponseWriter, _ *http.Request) {
|
|
saveIsPinnedPost(c, w, false)
|
|
}
|
|
|
|
func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
// license check
|
|
if !model.MinimumProfessionalLicense(c.App.Srv().License()) {
|
|
c.Err = model.NewAppError("", model.NoTranslation, nil, "feature is not available for the current license", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
c.RequirePostId().RequireUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
acknowledgement, appErr := c.App.SaveAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
js, err := json.Marshal(acknowledgement)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("acknowledgePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
if _, err := w.Write(js); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
// license check
|
|
if !model.MinimumProfessionalLicense(c.App.Srv().License()) {
|
|
c.Err = model.NewAppError("", "license_error.feature_unavailable", nil, "feature is not available for the current license", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
c.RequirePostId().RequireUserId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
|
|
c.SetPermissionError(model.PermissionEditOtherUsers)
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
_, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
appErr := c.App.DeleteAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func moveThread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.Config().FeatureFlags.MoveThreadsEnabled || c.App.License() == nil {
|
|
c.Err = model.NewAppError("moveThread", "api.post.move_thread.disabled.app_error", nil, "", http.StatusNotImplemented)
|
|
return
|
|
}
|
|
|
|
var moveThreadParams model.MoveThreadParams
|
|
if jsonErr := json.NewDecoder(r.Body).Decode(&moveThreadParams); jsonErr != nil {
|
|
c.SetInvalidParamWithErr("post", jsonErr)
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventMoveThread, model.AuditStatusFail)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
model.AddEventParameterToAuditRec(auditRec, "original_post_id", c.Params.PostId)
|
|
model.AddEventParameterToAuditRec(auditRec, "to_channel_id", moveThreadParams.ChannelId)
|
|
|
|
user, err := c.App.GetUser(c.AppContext.Session().UserId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
posts, _, err := c.App.GetPostsByIds([]string{c.Params.PostId})
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
channelMember, err := c.App.GetChannelMember(c.AppContext, posts[0].ChannelId, user.Id)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
userHasRole := hasPermittedWranglerRole(c, user, channelMember)
|
|
|
|
// Sysadmins are always permitted
|
|
if !userHasRole && !user.IsSystemAdmin() {
|
|
c.Err = model.NewAppError("moveThread", "api.post.move_thread.no_permission", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
userHasEmailDomain := true
|
|
// Only check the user's email domain if a list of allowed domains is configured
|
|
if len(c.App.Config().WranglerSettings.AllowedEmailDomain) > 0 {
|
|
userHasEmailDomain = slices.Contains(c.App.Config().WranglerSettings.AllowedEmailDomain, user.EmailDomain())
|
|
}
|
|
|
|
if !userHasEmailDomain && !user.IsSystemAdmin() {
|
|
c.Err = model.NewAppError("moveThread", "api.post.move_thread.no_permission", nil, fmt.Sprintf("User: %+v", user), http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
sourcePost, err := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), false)
|
|
if err != nil {
|
|
c.Err = err
|
|
if err.Id == "app.post.cloud.get.app_error" {
|
|
w.Header().Set(model.HeaderFirstInaccessiblePostTime, "1")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
err = c.App.MoveThread(c.AppContext, c.Params.PostId, sourcePost.ChannelId, moveThreadParams.ChannelId, user)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
|
|
ReturnStatusOK(w)
|
|
}
|
|
|
|
func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted"))
|
|
if includeDeleted && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
|
c.SetPermissionError(model.PermissionManageSystem)
|
|
return
|
|
}
|
|
|
|
infos, appErr := c.App.GetFileInfosForPostWithMigration(c.AppContext, c.Params.PostId, includeDeleted)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
if c.HandleEtag(model.GetEtagForFileInfos(infos), "Get File Infos For Post", w, r) {
|
|
return
|
|
}
|
|
|
|
js, err := json.Marshal(infos)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("getFileInfosForPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
|
w.Header().Set(model.HeaderEtagServer, model.GetEtagForFileInfos(infos))
|
|
if _, err := w.Write(js); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getPostInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
info, appErr := c.App.GetPostInfo(c.AppContext, c.Params.PostId)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
js, err := json.Marshal(info)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("getPostInfo", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
return
|
|
}
|
|
|
|
if _, err := w.Write(js); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func restorePostVersion(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequirePostId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
props := mux.Vars(r)
|
|
restoreVersionId, ok := props["restore_version_id"]
|
|
if !ok {
|
|
c.SetInvalidParam("restore_version_id")
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventRestorePostVersion, model.AuditStatusFail)
|
|
model.AddEventParameterToAuditRec(auditRec, "id", c.Params.PostId)
|
|
model.AddEventParameterToAuditRec(auditRec, "restore_version_id", restoreVersionId)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
|
|
toRestorePost, err := c.App.GetSinglePost(c.AppContext, restoreVersionId, true)
|
|
if err != nil {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
|
|
// user can only restore their own posts
|
|
if c.AppContext.Session().UserId != toRestorePost.UserId {
|
|
c.SetPermissionError(model.PermissionEditPost)
|
|
return
|
|
}
|
|
|
|
postPatchChecks(c, auditRec, &toRestorePost.Message)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
updatedPost, appErr := c.App.RestorePostVersion(c.AppContext, c.AppContext.Session().UserId, c.Params.PostId, restoreVersionId)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
auditRec.Success()
|
|
auditRec.AddEventResultState(updatedPost)
|
|
|
|
if err := updatedPost.EncodeJSON(w); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func hasPermittedWranglerRole(c *Context, user *model.User, channelMember *model.ChannelMember) bool {
|
|
// If there are no configured PermittedWranglerRoles, skip the check
|
|
if len(c.App.Config().WranglerSettings.PermittedWranglerRoles) == 0 {
|
|
return true
|
|
}
|
|
|
|
userRoles := user.Roles + " " + channelMember.Roles
|
|
for _, role := range c.App.Config().WranglerSettings.PermittedWranglerRoles {
|
|
if model.IsInRole(userRoles, role) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// rewriteMessage handles AI-powered message rewriting requests
|
|
func rewriteMessage(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
// Parse request
|
|
var req model.RewriteRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
c.SetInvalidParamWithErr("request_body", err)
|
|
return
|
|
}
|
|
|
|
if !model.IsValidId(req.AgentID) {
|
|
c.SetInvalidParam("agent_id")
|
|
return
|
|
}
|
|
|
|
// Call app layer to handle business logic
|
|
response, appErr := c.App.RewriteMessage(
|
|
c.AppContext,
|
|
req.AgentID,
|
|
req.Message,
|
|
req.Action,
|
|
req.CustomPrompt,
|
|
)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
|
|
// Return response
|
|
w.WriteHeader(http.StatusOK)
|
|
if err := json.NewEncoder(w).Encode(*response); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|