* Create scheduled post api (#27920) * Added migration files for Postgres * Added migrations for MySQL * Added store method * Added API and store tests * Renamed migration after syncing with master * Added app layer tests * API is ready * API is ready * API is ready * Renamed migration after syncing with master * Updated migration list * Fixed retry layer tests * Allowed posts with empty messages * Review fixes * Reverted an incorrect change * Renamed migration and fixed ID assignment * CI * Send post button changes (#28019) * added Split button * WIP * Added core menu options * WIP * WIP * WIP * Handled displaying error in creating scheduled post * lint fixes * webapp i18n fix * Review fixes * Fixed a webapp test * A few more fixes * Removed a duplicate comment * Scheduled post job (#28088) * Added the job function * Added query for fetching scheduled posts for pricessing * WIP * WIP * WIP * WIP * WIP * WIP * Reafactoring of scheduled post job * Lint fixes * Updated i18n files * FInishing touches * Added tests for GetScheduledPosts * Added tests for PermanentlyDeleteScheduledPosts * Updated all layer * Some changes as discussed with team * Added tests for UpdatedScheduledPost * Code review refactoring * Added job test * MM-60120 - Custom time selection (#28120) * Added a common date time picker modal and used it for post reminder * Added a common date time picker modal and used it for post reminderggp * Added modal for custom schedule time and fixed TZ issue * WIP * Removed event from useSubmit hook * Removed event from useSubmit hook * Added timezone handling * fixed type error * Updated i18n strings * Minor cleanup * updated snapshots * review fixes * Handled event * Supported for having a DM thread open in RHS while in a regular channel * Review fixes * MM-60136 - Scheduled messages tab (#28133) * WIP * WIP * Created Tabs and Tab wrapper with added styling * Added API to get scheduled posts * WIP * Displated scheduled post count * i18n fix * Added tests * Handled asetting active tab absed on URL: * Reverted unintended change * Added API to client ad OpenAPI specs * Renamed file * Adding fileinfo to schedule posts * Partial review fixes * Made get scheduled post API return posts by teamID * review fixes * Moved scheduled post redux code to MM-redux package * Usedd selector factory * WIP: * WIP: * Lint fix * Fixed an incorrect openapi spec file * Removed redundent permission check * Clreaed scheduled post data on logout * Removed unused i18n string: * lint fix * Render scheduled posts (#28208) * WIP * WIP * Created Tabs and Tab wrapper with added styling * Added API to get scheduled posts * WIP * Displated scheduled post count * i18n fix * Added tests * Handled asetting active tab absed on URL: * Reverted unintended change * Added API to client ad OpenAPI specs * Renamed file * Created common component for draft list item * WIP * WIP * Adding fileinfo to schedule posts * Basic rendering * Added count badge to tabs * WIP * Made the Drafts LHS iteam appear if no drafts exist but scheduled posts do * Fixed icon size * Partial review fixes * Made get scheduled post API return posts by teamID * Handled initial vs team switch load * Displayed scheduled date in panel header * Added error message and error indiocator * WIP * review fixes * WIP Adding error reason tag * Added error codes * Moved scheduled post redux code to MM-redux package * Usedd selector factory * WIP: * WIP: * Lint fix * Fixed an incorrect openapi spec file * Removed redundent permission check * Clreaed scheduled post data on logout * Removed unused i18n string: * lint fix * Opened rescheduling modal * Updated graphic for empty state of schduled post list * Added delete scheduled post option and modal * Badge and timezone fix * WIP: * Added send now confirmation modal * lint * Webapp i18n fix * Fixed webapp test * Fixed a bug where DM/GM scheduled posts weren't immideatly showing up in UI * Minor fixes * WIP * Review fixes * Review fixes * Optimisations * Fixed reducer name * Moment optimizatin * Updated route check * MM-60144 - added API to update a scheduled post (#28248) * WIP * Added api and ap layer for update scheduled post ̛̦̄ * Added API to OpenAI specs, Go client and TS client * removed permissio check * Added tests * Fixed tests * Added PreUpdate method on scheduled post model * MM-60131 - Reschedule post integration (#28281) * Handled rescheduling post in webapp * Added error handling * MM-60146 - Delete scheduled post api (#28265) * WIP * Added api and ap layer for update scheduled post ̛̦̄ * Added API to OpenAI specs, Go client and TS client * removed permissio check * Added tests * Fixed tests * Added PreUpdate method on scheduled post model * Added delete scheduled post API * Added API to Go client and OpenAPI specs * Added API to TS client * Added tests * CI * Rmeoved two incorrect code comments * MM-60653 - Integrated delete scheduled post API (#28296) * Integrated delete scheduled apost API * Lint fix * Review fixes * Excluded draft checks from scheduled posts (#28370) * Excluded draft checks from scheduled posts * Added a removed todo * MM-60125 - Scheduled post channel indicator (#28320) * Integrated delete scheduled apost API * Lint fix * Added state for storing scheduled posts by channel ID * Refactored redux store to store scheudled posts by ID, thens tore IDs everywhere * Refactored redux store to store scheudled posts by ID, thens tore IDs everywhere * WIP * Added scheduled post indiocator * Handled single and multiple scheudled posts * Review fixes * Fixed styling and handled center channel, RHS and threads view * Lint fix * i18n fix * Fixed a cycling dependency * Lint fix * Added some more comments * Updated styling * Review fixes * Added common component for remote user time and scheduled post indicator * Updated scheduled post count * Minor change * Moved CSS code around * Fixed a bug where files in scheduled post didn't show up until refresh (#28359) --------- Co-authored-by: Daniel Espino García <larkox@gmail.com> * Scheduled post config (#28485) * Added config * Added config on server and webapp side * Added config check in server and webapp * Added license check * Added license check * Added placeholder help text * Added license check to job * Fixed job test * Review fixes * Updated English text * Review fixes * MM-60118 - Added index on ScheduledPosts table (#28579) * Added index * Updated indexes * Scheduled posts misc fixes (#28625) * Added detailed logging for scheduled post job * Limited scheduled posts processing to 24 hours * Marked old scheduled posts as unable to send * Added t5ests * converted some logs to trace level * Fixed a bug causing error message to show up on deleting a scheduled post in a deleted thread (#28630) * Fixed scheduled posts link in RHS (#28659) * Fixed scheduled posts link in RHS * Review fixes * Fix permission name in scheduled posts by team (#28580) * Fix permission name * fix wording --------- Co-authored-by: Mattermost Build <build@mattermost.com> * FIxed width of generic modal header to fix browser channel modal (#28639) * Only consider error-free scheduled posts for indicator in channel and RHS (#28683) * Show only errro free scheudled posts in post box indicator * Fixed a bug to handle no scheduled posts * Fixed draft and scheudled post UI in mobile view (#28680) * MM-60873 and MM-60872 - Fixed a bug with updating scheduled posts (#28656) * Fixed a bug with updating scheduled posts * Better selectors * MOved shceuled post message length validation to app layer * MM-60732 - Scheduled posts channel link now takes you to the first scheduled post in channel/thread in list (#28768) * Ordered scheudle dposts by schgeudled at nad create at * Ordered in client * Added scroll to target * Removed classname prop * Fixed tests * Added doc * Import fix * MM-60961 - Fixed a bug where API used incoming create at date for scheduled post (#28703) * Fixed a bug where API used incoming create at date for scheduled post * Stopped sending created at value for scheduled post * MM-60785 - Fixed a bug where scheduled posts of channel we are no longer member of didn't show up (#28637) * Fixed a bug where scheduled posts of channel we are no longer member of didn't show up * Added a comment * CI * Used data loader to optimise laoding missing channels * Minor refactoring * MM-60963 - Added common checks for post and scheduled posts (#28713) * Added commen checks for post and scheuled posts * Sanitised scheduled posts * Fixed tests * Splitted post checks into app and context functions * Added checks on scheduiled posts job as well: * i18n fix * Fixed a test * Renamed a func * removed duplicate check * Scheduled posts UI fixes (#28828) * Fixed send button and time picker borders * Fixed center alignment of time picker * Removed on for today and tomorrow * Lint fix * Date time modal hover state fix * Badge fix * Fixed a mnerge issue * Scheduled Post send now and add schedule on draft (#28851) * Added send now option on scheduled posts * Minor refactoring * WIP * WIP * WIP * Lint fix * i18n fix * Snapshot update * Review fixes * Scheduled post inline editing (#28893) * Added send now option on scheduled posts * Minor refactoring * WIP * WIP * WIP * Lint fix * i18n fix * Snapshot update * Displayed editing component in scheduled post * Added handling for updating scheduled post * Handle events * Fixed escape key issue in scheudled post editing * Fixes * Displayed error message for editing error * Don't show mention warning * Handled dev mode (#28918) * MInor fixes * client fix * Fixes * CI * Removed dev mode behaviour temperorily (#29008) --------- Co-authored-by: Daniel Espino García <larkox@gmail.com> Co-authored-by: Eva Sarafianou <eva.sarafianou@gmail.com> Co-authored-by: Mattermost Build <build@mattermost.com>
1303 строки
39 KiB
Go
1303 строки
39 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"
|
|
"strconv"
|
|
"time"
|
|
|
|
"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/audit"
|
|
"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.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)
|
|
}
|
|
|
|
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("createPost", audit.Fail)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
audit.AddEventParameterAuditable(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, true, false, 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 := r.URL.Query().Get("skipFetchThreads") == "true"
|
|
collapsedThreads := r.URL.Query().Get("collapsedThreads") == "true"
|
|
collapsedThreadsExtended := r.URL.Query().Get("collapsedThreadsExtended") == "true"
|
|
includeDeleted := r.URL.Query().Get("include_deleted") == "true"
|
|
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
|
|
}
|
|
|
|
if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
|
|
channel, appErr := c.App.GetChannel(c.AppContext, channelId)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return
|
|
}
|
|
if channel.DeleteAt != 0 {
|
|
c.Err = model.NewAppError("Api4.getPostsForChannel", "api.user.view_archived_channels.get_posts_for_channel.app_error", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
var list *model.PostList
|
|
etag := ""
|
|
|
|
if since > 0 {
|
|
list, err = c.App.GetPostsSince(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(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(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(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(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, false, false, 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, false, false, 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("deletePost", audit.Fail)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
audit.AddEventParameter(auditRec, "post_id", c.Params.PostId)
|
|
audit.AddEventParameter(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
|
|
}
|
|
|
|
direction := ""
|
|
if dir := r.URL.Query().Get("direction"); dir != "" {
|
|
if dir != "up" && dir != "down" {
|
|
c.SetInvalidParam("direction")
|
|
return
|
|
}
|
|
direction = dir
|
|
}
|
|
opts := model.GetPostsOptions{
|
|
SkipFetchThreads: r.URL.Query().Get("skipFetchThreads") == "true",
|
|
CollapsedThreads: r.URL.Query().Get("collapsedThreads") == "true",
|
|
CollapsedThreadsExtended: r.URL.Query().Get("collapsedThreadsExtended") == "true",
|
|
PerPage: perPage,
|
|
Direction: direction,
|
|
FromPost: fromPost,
|
|
FromCreateAt: fromCreateAt,
|
|
}
|
|
list, err := c.App.GetPostThread(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
|
|
}
|
|
|
|
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)
|
|
|
|
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("updatePost", audit.Fail)
|
|
audit.AddEventParameterAuditable(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")
|
|
|
|
// Updating the file_ids of a post is not a supported operation and will be ignored
|
|
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), 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("patchPost", audit.Fail)
|
|
audit.AddEventParameter(auditRec, "id", c.Params.PostId)
|
|
audit.AddEventParameterAuditable(auditRec, "patch", &post)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
|
|
if post.Props != nil {
|
|
postHardenedModeCheckWithContext("patchPost", c, *post.Props)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Updating the file_ids of a post is not a supported operation and will be ignored
|
|
post.FileIds = nil
|
|
|
|
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) && post.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
|
|
}
|
|
|
|
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, c.App.PostPatchWithProxyRemovedFromImageURLs(&post))
|
|
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 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("saveIsPinnedPost", audit.Fail)
|
|
audit.AddEventParameter(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)
|
|
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
|
|
permissionErr := minimumProfessionalLicense(c)
|
|
if permissionErr != nil {
|
|
c.Err = permissionErr
|
|
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
|
|
permissionErr := minimumProfessionalLicense(c)
|
|
if permissionErr != nil {
|
|
c.Err = permissionErr
|
|
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("moveThread", audit.Fail)
|
|
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
|
audit.AddEventParameter(auditRec, "original_post_id", c.Params.PostId)
|
|
audit.AddEventParameter(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 := len(c.App.Config().WranglerSettings.AllowedEmailDomain) == 0
|
|
for _, domain := range c.App.Config().WranglerSettings.AllowedEmailDomain {
|
|
if user.EmailDomain() == domain {
|
|
userHasEmailDomain = true
|
|
break
|
|
}
|
|
}
|
|
|
|
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 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
|
|
}
|