* 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>
Этот коммит содержится в:
Harshil Sharma
2024-11-04 11:39:35 +05:30
коммит произвёл GitHub
родитель aaf9234c8e
Коммит e281b3f37e
135 изменённых файлов: 7792 добавлений и 684 удалений

Просмотреть файл

@@ -337,6 +337,7 @@ func Init(srv *app.Server) (*API, error) {
api.InitLimits()
api.InitOutgoingOAuthConnection()
api.InitClientPerformanceMetrics()
api.InitScheduledPost()
// If we allow testing then listen for manual testing URL hits
if *srv.Config().ServiceSettings.EnableTesting {

Просмотреть файл

@@ -239,12 +239,7 @@ func requireLicense(c *Context) *model.AppError {
}
func minimumProfessionalLicense(c *Context) *model.AppError {
lic := c.App.Srv().License()
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
return err
}
return nil
return model.MinimumProfessionalProvidedLicense(c.App.Srv().License())
}
func setHandlerOpts(handler *web.Handler, opts ...APIHandlerOption) {

Просмотреть файл

@@ -48,6 +48,26 @@ func (api *API) InitPost() {
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 {
@@ -56,87 +76,19 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
}
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)
hasPermission := false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := c.App.GetChannel(c.AppContext, post.ChannelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode {
if reservedProps := post.ContainsIntegrationsReservedProps(); len(reservedProps) > 0 && !c.AppContext.Session().IsIntegration() {
c.SetInvalidParamWithDetails("props", fmt.Sprintf("Cannot use props reserved for integrations. props: %v", reservedProps))
return
}
}
if post.CreateAt != 0 && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
post.CreateAt = 0
}
if post.GetPriority() != nil {
priorityForbiddenErr := model.NewAppError("Api4.createPost", "api.post.post_priority.priority_post_not_allowed_for_user.request_error", nil, "userId="+c.AppContext.Session().UserId, http.StatusForbidden)
if !c.App.IsPostPriorityEnabled() {
c.Err = priorityForbiddenErr
return
}
if post.RootId != "" {
c.Err = model.NewAppError("Api4.createPost", "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", nil, "", http.StatusBadRequest)
return
}
if ack := post.GetRequestedAck(); ack != nil && *ack {
licenseErr := minimumProfessionalLicense(c)
if licenseErr != nil {
c.Err = licenseErr
return
}
}
if notification := post.GetPersistentNotification(); notification != nil && *notification {
licenseErr := minimumProfessionalLicense(c)
if licenseErr != nil {
c.Err = licenseErr
return
}
if !c.App.IsPersistentNotificationsEnabled() {
c.Err = priorityForbiddenErr
return
}
if !post.IsUrgent() {
c.Err = model.NewAppError("Api4.createPost", "api.post.post_priority.urgent_persistent_notification_post.request_error", nil, "", http.StatusBadRequest)
return
}
if !*c.App.Config().ServiceSettings.AllowPersistentNotificationsForGuests {
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
if user.IsGuest() {
c.Err = priorityForbiddenErr
return
}
}
}
createPostChecks("Api4.createPost", c, &post)
if c.Err != nil {
return
}
setOnline := r.URL.Query().Get("set_online")
@@ -888,11 +840,9 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode {
if reservedProps := post.ContainsIntegrationsReservedProps(); len(reservedProps) > 0 && !c.AppContext.Session().IsIntegration() {
c.SetInvalidParamWithDetails("props", fmt.Sprintf("Cannot use props reserved for integrations. props: %v", reservedProps))
return
}
postHardenedModeCheckWithContext("UpdatePost", c, post.GetProps())
if c.Err != nil {
return
}
originalPost, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
@@ -957,9 +907,9 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
audit.AddEventParameterAuditable(auditRec, "patch", &post)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode {
if reservedProps := post.ContainsIntegrationsReservedProps(); len(reservedProps) > 0 && !c.AppContext.Session().IsIntegration() {
c.SetInvalidParamWithDetails("props", fmt.Sprintf("Cannot use props reserved for integrations. props: %v", reservedProps))
if post.Props != nil {
postHardenedModeCheckWithContext("patchPost", c, *post.Props)
if c.Err != nil {
return
}
}

43
server/channels/api4/post_utils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,43 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/app"
)
func userCreatePostPermissionCheckWithContext(c *Context, channelId string) {
hasPermission := false
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := c.App.GetChannel(c.AppContext, channelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
c.SetPermissionError(model.PermissionCreatePost)
return
}
}
func postHardenedModeCheckWithContext(where string, c *Context, props model.StringInterface) {
isIntegration := c.AppContext.Session().IsIntegration()
if appErr := app.PostHardenedModeCheckWithApp(c.App, isIntegration, props); appErr != nil {
appErr.Where = where
c.Err = appErr
}
}
func postPriorityCheckWithContext(where string, c *Context, priority *model.PostPriority, rootId string) {
appErr := app.PostPriorityCheckWithApp(where, c.App, c.AppContext.Session().UserId, priority, rootId)
if appErr != nil {
appErr.Where = where
c.Err = appErr
}
}

225
server/channels/api4/scheduled_post.go Обычный файл
Просмотреть файл

@@ -0,0 +1,225 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/audit"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
func (api *API) InitScheduledPost() {
api.BaseRoutes.Posts.Handle("/schedule", api.APISessionRequired(createSchedulePost)).Methods(http.MethodPost)
api.BaseRoutes.Posts.Handle("/schedule/{scheduled_post_id:[A-Za-z0-9]+}", api.APISessionRequired(updateScheduledPost)).Methods(http.MethodPut)
api.BaseRoutes.Posts.Handle("/schedule/{scheduled_post_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteScheduledPost)).Methods(http.MethodDelete)
api.BaseRoutes.Posts.Handle("/scheduled/team/{team_id:[A-Za-z0-9]+}", api.APISessionRequired(getTeamScheduledPosts)).Methods(http.MethodGet)
}
func scheduledPostChecks(where string, c *Context, scheduledPost *model.ScheduledPost) {
// ***************************************************************
// NOTE - if you make any change here, please make sure to apply the
// same change for scheduled posts job as well in the `canPostScheduledPost()` function
// in app layer.
// ***************************************************************
userCreatePostPermissionCheckWithContext(c, scheduledPost.ChannelId)
if c.Err != nil {
return
}
postHardenedModeCheckWithContext(where, c, scheduledPost.GetProps())
if c.Err != nil {
return
}
postPriorityCheckWithContext(where, c, scheduledPost.GetPriority(), scheduledPost.RootId)
}
func requireScheduledPostsEnabled(c *Context) {
if !*c.App.Srv().Config().ServiceSettings.ScheduledPosts {
c.Err = model.NewAppError("", "api.scheduled_posts.feature_disabled", nil, "", http.StatusBadRequest)
return
}
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("", "api.scheduled_posts.license_error", nil, "", http.StatusBadRequest)
return
}
}
func createSchedulePost(c *Context, w http.ResponseWriter, r *http.Request) {
requireScheduledPostsEnabled(c)
if c.Err != nil {
return
}
var scheduledPost model.ScheduledPost
if err := json.NewDecoder(r.Body).Decode(&scheduledPost); err != nil {
c.SetInvalidParamWithErr("schedule_post", err)
return
}
scheduledPost.UserId = c.AppContext.Session().UserId
scheduledPost.SanitizeInput()
auditRec := c.MakeAuditRecord("createSchedulePost", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameterAuditable(auditRec, "scheduledPost", &scheduledPost)
scheduledPostChecks("Api4.createSchedulePost", c, &scheduledPost)
if c.Err != nil {
return
}
createdScheduledPost, appErr := c.App.SaveScheduledPost(c.AppContext, &scheduledPost)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(createdScheduledPost)
auditRec.AddEventObjectType("scheduledPost")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(createdScheduledPost); err != nil {
mlog.Error("failed to encode scheduled post to return API response", mlog.Err(err))
return
}
}
func getTeamScheduledPosts(c *Context, w http.ResponseWriter, r *http.Request) {
requireScheduledPostsEnabled(c)
if c.Err != nil {
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return
}
teamId := c.Params.TeamId
userId := c.AppContext.Session().UserId
scheduledPosts, appErr := c.App.GetUserTeamScheduledPosts(c.AppContext, userId, teamId)
if appErr != nil {
c.Err = appErr
return
}
response := map[string][]*model.ScheduledPost{}
response[teamId] = scheduledPosts
if r.URL.Query().Get("includeDirectChannels") == "true" {
directChannelScheduledPosts, appErr := c.App.GetUserTeamScheduledPosts(c.AppContext, userId, "")
if appErr != nil {
c.Err = appErr
return
}
response["directChannels"] = directChannelScheduledPosts
}
if err := json.NewEncoder(w).Encode(response); err != nil {
mlog.Error("failed to encode scheduled posts to return API response", mlog.Err(err))
return
}
}
func updateScheduledPost(c *Context, w http.ResponseWriter, r *http.Request) {
requireScheduledPostsEnabled(c)
if c.Err != nil {
return
}
scheduledPostId := mux.Vars(r)["scheduled_post_id"]
if scheduledPostId == "" {
c.SetInvalidURLParam("scheduled_post_id")
return
}
var scheduledPost model.ScheduledPost
if err := json.NewDecoder(r.Body).Decode(&scheduledPost); err != nil {
c.SetInvalidParamWithErr("schedule_post", err)
return
}
if scheduledPost.Id != scheduledPostId {
c.SetInvalidURLParam("scheduled_post_id")
return
}
auditRec := c.MakeAuditRecord("updateScheduledPost", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameterAuditable(auditRec, "scheduledPost", &scheduledPost)
scheduledPostChecks("Api4.updateScheduledPost", c, &scheduledPost)
if c.Err != nil {
return
}
userId := c.AppContext.Session().UserId
updatedScheduledPost, appErr := c.App.UpdateScheduledPost(c.AppContext, userId, &scheduledPost)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(updatedScheduledPost)
auditRec.AddEventObjectType("scheduledPost")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(updatedScheduledPost); err != nil {
mlog.Error("failed to encode scheduled post to return API response", mlog.Err(err))
return
}
}
func deleteScheduledPost(c *Context, w http.ResponseWriter, r *http.Request) {
requireScheduledPostsEnabled(c)
if c.Err != nil {
return
}
scheduledPostId := mux.Vars(r)["scheduled_post_id"]
if scheduledPostId == "" {
c.SetInvalidURLParam("scheduled_post_id")
return
}
auditRec := c.MakeAuditRecord("deleteScheduledPost", audit.Fail)
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
audit.AddEventParameter(auditRec, "scheduledPostId", scheduledPostId)
userId := c.AppContext.Session().UserId
connectionID := r.Header.Get(model.ConnectionId)
deletedScheduledPost, appErr := c.App.DeleteScheduledPost(c.AppContext, userId, scheduledPostId, connectionID)
if appErr != nil {
c.Err = appErr
return
}
auditRec.Success()
auditRec.AddEventResultState(deletedScheduledPost)
auditRec.AddEventObjectType("scheduledPost")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(deletedScheduledPost); err != nil {
mlog.Error("failed to encode scheduled post to return API response", mlog.Err(err))
return
}
}

Просмотреть файл

@@ -584,6 +584,7 @@ type AppIface interface {
DeleteReactionForPost(c request.CTX, reaction *model.Reaction) *model.AppError
DeleteRemoteCluster(remoteClusterId string) (bool, *model.AppError)
DeleteRetentionPolicy(policyID string) *model.AppError
DeleteScheduledPost(rctx request.CTX, userId, scheduledPostId, connectionId string) (*model.ScheduledPost, *model.AppError)
DeleteScheme(schemeId string) (*model.Scheme, *model.AppError)
DeleteSharedChannelRemote(id string) (bool, error)
DeleteSidebarCategory(c request.CTX, userID, teamID, categoryId string) *model.AppError
@@ -870,6 +871,7 @@ type AppIface interface {
GetUserByUsername(username string) (*model.User, *model.AppError)
GetUserCountForReport(filter *model.UserReportOptions) (*int64, *model.AppError)
GetUserForLogin(c request.CTX, id, loginId string) (*model.User, *model.AppError)
GetUserTeamScheduledPosts(rctx request.CTX, userId, teamId string) ([]*model.ScheduledPost, *model.AppError)
GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError)
GetUsers(userIDs []string) ([]*model.User, *model.AppError)
GetUsersByGroupChannelIds(c request.CTX, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError)
@@ -1002,6 +1004,7 @@ type AppIface interface {
PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post
PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post
PreparePostListForClient(c request.CTX, originalList *model.PostList) *model.PostList
ProcessScheduledPosts(rctx request.CTX)
ProcessSlackText(text string) string
Publish(message *model.WebSocketEvent)
PublishUserTyping(userID, channelID, parentId string) *model.AppError
@@ -1067,6 +1070,7 @@ type AppIface interface {
SaveComplianceReport(rctx request.CTX, job *model.Compliance) (*model.Compliance, *model.AppError)
SaveReactionForPost(c request.CTX, reaction *model.Reaction) (*model.Reaction, *model.AppError)
SaveReportChunk(format string, prefix string, count int, reportData []model.ReportableObject) *model.AppError
SaveScheduledPost(rctx request.CTX, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, *model.AppError)
SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error)
SaveUserTermsOfService(userID, termsOfServiceId string, accepted bool) *model.AppError
SchemesIterator(scope string, batchSize int) func() []*model.Scheme
@@ -1203,6 +1207,7 @@ type AppIface interface {
UpdateRemoteCluster(rc *model.RemoteCluster) (*model.RemoteCluster, *model.AppError)
UpdateRemoteClusterTopics(remoteClusterId string, topics string) (*model.RemoteCluster, *model.AppError)
UpdateRole(role *model.Role) (*model.Role, *model.AppError)
UpdateScheduledPost(rctx request.CTX, userId string, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, *model.AppError)
UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError)
UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error)
UpdateSharedChannelRemoteCursor(id string, cursor model.GetPostsSinceForSyncCursor) error

Просмотреть файл

@@ -77,7 +77,9 @@ type Channels struct {
postReminderMut sync.Mutex
postReminderTask *model.ScheduledTask
loginAttemptsMut sync.Mutex
scheduledPostMut sync.Mutex
scheduledPostTask *model.ScheduledTask
loginAttemptsMut sync.Mutex
}
func NewChannels(s *Server) (*Channels, error) {

Просмотреть файл

@@ -3727,6 +3727,28 @@ func (a *OpenTracingAppLayer) DeleteRetentionPolicy(policyID string) *model.AppE
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteScheduledPost(rctx request.CTX, userId string, scheduledPostId string, connectionId string) (*model.ScheduledPost, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteScheduledPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.DeleteScheduledPost(rctx, userId, scheduledPostId, connectionId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) DeleteScheme(schemeId string) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteScheme")
@@ -10990,6 +11012,28 @@ func (a *OpenTracingAppLayer) GetUserStatusesByIds(userIDs []string) ([]*model.S
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserTeamScheduledPosts(rctx request.CTX, userId string, teamId string) ([]*model.ScheduledPost, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserTeamScheduledPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserTeamScheduledPosts(rctx, userId, teamId)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserTermsOfService")
@@ -13964,6 +14008,21 @@ func (a *OpenTracingAppLayer) PreparePostListForClient(c request.CTX, originalLi
return resultVar0
}
func (a *OpenTracingAppLayer) ProcessScheduledPosts(rctx request.CTX) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ProcessScheduledPosts")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.ProcessScheduledPosts(rctx)
}
func (a *OpenTracingAppLayer) ProcessSlackAttachments(attachments []*model.SlackAttachment) []*model.SlackAttachment {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ProcessSlackAttachments")
@@ -15511,6 +15570,28 @@ func (a *OpenTracingAppLayer) SaveReportChunk(format string, prefix string, coun
return resultVar0
}
func (a *OpenTracingAppLayer) SaveScheduledPost(rctx request.CTX, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveScheduledPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveScheduledPost(rctx, scheduledPost)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveSharedChannelRemote(remote *model.SharedChannelRemote) (*model.SharedChannelRemote, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveSharedChannelRemote")
@@ -18690,6 +18771,28 @@ func (a *OpenTracingAppLayer) UpdateRole(role *model.Role) (*model.Role, *model.
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateScheduledPost(rctx request.CTX, userId string, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateScheduledPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.UpdateScheduledPost(rctx, userId, scheduledPost)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateScheme")

118
server/channels/app/post_permission_utils.go Обычный файл
Просмотреть файл

@@ -0,0 +1,118 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
)
func PostPriorityCheckWithApp(where string, a AppIface, userId string, priority *model.PostPriority, rootId string) *model.AppError {
user, appErr := a.GetUser(userId)
if appErr != nil {
return appErr
}
isPostPriorityEnabled := a.IsPostPriorityEnabled()
IsPersistentNotificationsEnabled := a.IsPersistentNotificationsEnabled()
allowPersistentNotificationsForGuests := *a.Config().ServiceSettings.AllowPersistentNotificationsForGuests
license := a.License()
appErr = postPriorityCheck(user, priority, rootId, isPostPriorityEnabled, IsPersistentNotificationsEnabled, allowPersistentNotificationsForGuests, license)
if appErr != nil {
appErr.Where = where
return appErr
}
return nil
}
func postPriorityCheck(
user *model.User,
priority *model.PostPriority,
rootId string,
isPostPriorityEnabled,
isPersistentNotificationsEnabled,
allowPersistentNotificationsForGuests bool,
license *model.License,
) *model.AppError {
if priority == nil {
return nil
}
priorityForbiddenErr := model.NewAppError("", "api.post.post_priority.priority_post_not_allowed_for_user.request_error", nil, "userId="+user.Id, http.StatusForbidden)
if !isPostPriorityEnabled {
return priorityForbiddenErr
}
if rootId != "" {
return model.NewAppError("", "api.post.post_priority.priority_post_only_allowed_for_root_post.request_error", nil, "", http.StatusBadRequest)
}
if ack := priority.RequestedAck; ack != nil && *ack {
licenseErr := model.MinimumProfessionalProvidedLicense(license)
if licenseErr != nil {
return licenseErr
}
}
if notification := priority.PersistentNotifications; notification != nil && *notification {
licenseErr := model.MinimumProfessionalProvidedLicense(license)
if licenseErr != nil {
return licenseErr
}
if !isPersistentNotificationsEnabled {
return priorityForbiddenErr
}
if *priority.Priority != model.PostPriorityUrgent {
return model.NewAppError("", "api.post.post_priority.urgent_persistent_notification_post.request_error", nil, "", http.StatusBadRequest)
}
if !allowPersistentNotificationsForGuests {
if user.IsGuest() {
return priorityForbiddenErr
}
}
}
return nil
}
func PostHardenedModeCheckWithApp(a AppIface, isIntegration bool, props model.StringInterface) *model.AppError {
hardenedModeEnabled := *a.Config().ServiceSettings.ExperimentalEnableHardenedMode
return postHardenedModeCheck(hardenedModeEnabled, isIntegration, props)
}
func postHardenedModeCheck(hardenedModeEnabled, isIntegration bool, props model.StringInterface) *model.AppError {
if hardenedModeEnabled {
if reservedProps := model.ContainsIntegrationsReservedProps(props); len(reservedProps) > 0 && !isIntegration {
return model.NewAppError("", "api.context.invalid_body_param.app_error", map[string]any{"Name": "props"}, fmt.Sprintf("Cannot use props reserved for integrations. props: %v", reservedProps), http.StatusBadRequest)
}
}
return nil
}
func userCreatePostPermissionCheckWithApp(c request.CTX, a AppIface, userId, channelId string) *model.AppError {
hasPermission := false
if a.HasPermissionToChannel(c, userId, channelId, model.PermissionCreatePost) {
hasPermission = true
} else if channel, err := a.GetChannel(c, channelId); err == nil {
// Temporary permission check method until advanced permissions, please do not copy
if channel.Type == model.ChannelTypeOpen && a.HasPermissionToTeam(c, userId, channel.TeamId, model.PermissionCreatePostPublic) {
hasPermission = true
}
}
if !hasPermission {
return model.MakePermissionErrorForUser(userId, []*model.Permission{model.PermissionCreatePost})
}
return nil
}

112
server/channels/app/scheduled_post.go Обычный файл
Просмотреть файл

@@ -0,0 +1,112 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
)
func (a *App) SaveScheduledPost(rctx request.CTX, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, *model.AppError) {
maxMessageLength := a.Srv().Store().ScheduledPost().GetMaxMessageSize()
scheduledPost.PreSave()
if validationErr := scheduledPost.IsValid(maxMessageLength); validationErr != nil {
return nil, validationErr
}
// validate the channel is not archived
channel, appErr := a.GetChannel(rctx, scheduledPost.ChannelId)
if appErr != nil {
return nil, appErr
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("App.scheduledPostPreSaveChecks", "app.save_scheduled_post.channel_deleted.app_error", map[string]any{"user_id": scheduledPost.UserId, "channel_id": scheduledPost.ChannelId}, "", http.StatusBadRequest)
}
savedScheduledPost, err := a.Srv().Store().ScheduledPost().CreateScheduledPost(scheduledPost)
if err != nil {
return nil, model.NewAppError("App.ScheduledPost", "app.save_scheduled_post.save.app_error", map[string]any{"user_id": scheduledPost.UserId, "channel_id": scheduledPost.ChannelId}, "", http.StatusBadRequest)
}
// TODO: add WebSocket event broadcast here
return savedScheduledPost, nil
}
func (a *App) GetUserTeamScheduledPosts(rctx request.CTX, userId, teamId string) ([]*model.ScheduledPost, *model.AppError) {
scheduledPosts, err := a.Srv().Store().ScheduledPost().GetScheduledPostsForUser(userId, teamId)
if err != nil {
return nil, model.NewAppError("App.GetUserTeamScheduledPosts", "app.get_user_team_scheduled_posts.error", map[string]any{"user_id": userId, "team_id": teamId}, "", http.StatusInternalServerError)
}
if scheduledPosts == nil {
scheduledPosts = []*model.ScheduledPost{}
}
for _, scheduledPost := range scheduledPosts {
a.prepareDraftWithFileInfos(rctx, userId, &scheduledPost.Draft)
}
return scheduledPosts, nil
}
func (a *App) UpdateScheduledPost(rctx request.CTX, userId string, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, *model.AppError) {
maxMessageLength := a.Srv().Store().ScheduledPost().GetMaxMessageSize()
scheduledPost.PreUpdate()
if validationErr := scheduledPost.IsValid(maxMessageLength); validationErr != nil {
return nil, validationErr
}
// validate the scheduled post belongs to the said user
existingScheduledPost, err := a.Srv().Store().ScheduledPost().Get(scheduledPost.Id)
if err != nil {
return nil, model.NewAppError("app.UpdateScheduledPost", "app.update_scheduled_post.get_scheduled_post.error", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPost.Id}, "", http.StatusInternalServerError)
}
if existingScheduledPost == nil {
return nil, model.NewAppError("app.UpdateScheduledPost", "app.update_scheduled_post.existing_scheduled_post.not_exist", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPost.Id}, "", http.StatusNotFound)
}
if existingScheduledPost.UserId != userId {
return nil, model.NewAppError("app.UpdateScheduledPost", "app.update_scheduled_post.update_permission.error", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPost.Id}, "", http.StatusForbidden)
}
// This step is not required for update but is useful as we want to return the
// updated scheduled post. It's better to do this before calling update than after.
scheduledPost.RestoreNonUpdatableFields(existingScheduledPost)
if err := a.Srv().Store().ScheduledPost().UpdatedScheduledPost(scheduledPost); err != nil {
return nil, model.NewAppError("app.UpdateScheduledPost", "app.update_scheduled_post.update.error", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPost.Id}, "", http.StatusInternalServerError)
}
// TODO: add WebSocket event broadcast here. This will be done in a later PR
return scheduledPost, nil
}
func (a *App) DeleteScheduledPost(rctx request.CTX, userId, scheduledPostId, connectionId string) (*model.ScheduledPost, *model.AppError) {
scheduledPost, err := a.Srv().Store().ScheduledPost().Get(scheduledPostId)
if err != nil {
return nil, model.NewAppError("app.DeleteScheduledPost", "app.delete_scheduled_post.get_scheduled_post.error", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPostId}, "", http.StatusInternalServerError)
}
if scheduledPost == nil {
return nil, model.NewAppError("app.DeleteScheduledPost", "app.delete_scheduled_post.existing_scheduled_post.not_exist", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPostId}, "", http.StatusNotFound)
}
if scheduledPost.UserId != userId {
return nil, model.NewAppError("app.DeleteScheduledPost", "app.delete_scheduled_post.delete_permission.error", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPostId}, "", http.StatusForbidden)
}
if err := a.Srv().Store().ScheduledPost().PermanentlyDeleteScheduledPosts([]string{scheduledPostId}); err != nil {
return nil, model.NewAppError("app.DeleteScheduledPost", "app.delete_scheduled_post.delete_error", map[string]any{"user_id": userId, "scheduled_post_id": scheduledPostId}, "", http.StatusInternalServerError)
}
// TODO: add WebSocket event broadcast here. This will be done in a later PR
return scheduledPost, nil
}

357
server/channels/app/scheduled_post_job.go Обычный файл
Просмотреть файл

@@ -0,0 +1,357 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"net/http"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/pkg/errors"
)
const (
getPendingScheduledPostsPageSize = 100
scheduledPostBatchWaitTime = 1 * time.Second
)
func (a *App) ProcessScheduledPosts(rctx request.CTX) {
rctx = rctx.WithLogger(rctx.Logger().With(mlog.String("component", "scheduled_post_job")))
rctx.Logger().Debug("ProcessScheduledPosts called...")
if !*a.Config().ServiceSettings.ScheduledPosts {
rctx.Logger().Debug("ProcessScheduledPosts exiting as the feature is turned off via ServiceSettings.ScheduledPosts setting...")
return
}
if a.License() == nil {
rctx.Logger().Debug("ProcessScheduledPosts exiting as no license is available")
return
}
beforeTime := model.GetMillis()
afterTime := beforeTime - (24 * 60 * 60 * 1000) // subtracting 24 hours from beforeTime
lastScheduledPostId := ""
for {
// we wait some time before processing each batch to avoid hammering the database with too many requests.
time.Sleep(scheduledPostBatchWaitTime)
rctx.Logger().Debug("ProcessScheduledPosts: fetching page of pending scheduled posts...")
scheduledPostsBatch, err := a.Srv().Store().ScheduledPost().GetPendingScheduledPosts(beforeTime, afterTime, lastScheduledPostId, getPendingScheduledPostsPageSize)
if err != nil {
rctx.Logger().Error(
"App.ProcessScheduledPosts: failed to fetch pending scheduled posts page from database",
mlog.Int("before_time", beforeTime),
mlog.String("last_scheduled_post_id", lastScheduledPostId),
mlog.Int("items_per_page", getPendingScheduledPostsPageSize),
mlog.Err(err),
)
// Break the loop if we can't fetch the page.
// Missed posts will be processed in job's next round.
// Since we don't know any item's details, we can't fetch the next page as well.
// We could retry here but that's the same as trying in job's next round.
break
}
rctx.Logger().Debug("ProcessScheduledPosts: entries found in page of pending scheduled posts", mlog.Int("entries", len(scheduledPostsBatch)))
if len(scheduledPostsBatch) == 0 {
rctx.Logger().Debug("ProcessScheduledPosts: skipping as there are no pending scheduled")
// break loop if there are no more scheduled posts
break
}
// Saving the last item to use as marker for next page
lastScheduledPostId = scheduledPostsBatch[len(scheduledPostsBatch)-1].Id
beforeTime = scheduledPostsBatch[len(scheduledPostsBatch)-1].ScheduledAt
if err := a.processScheduledPostBatch(rctx, scheduledPostsBatch); err != nil {
rctx.Logger().Error(
"App.ProcessScheduledPosts: failed to process scheduled posts batch",
mlog.Int("before_time", beforeTime),
mlog.String("last_scheduled_post_id", lastScheduledPostId),
mlog.Int("items_per_page", getPendingScheduledPostsPageSize),
mlog.Err(err),
)
// failure to process one batch doesn't mean other batches will fail as well.
// Continue processing next batch. The posts that failed in this batch will be picked
// up when the job next runs.
continue
}
rctx.Logger().Debug("ProcessScheduledPosts: finished processing a page of pending scheduled posts.")
if len(scheduledPostsBatch) < getPendingScheduledPostsPageSize {
// if we got less than page size worth of scheduled posts, it indicates
// that we have no more pending scheduled posts. So, we can break instead of making
// an additional database call as we know there are going to be no records in there.
break
}
}
// once all scheduled posts are processed, we need to update and close the old ones
// as we don't process pending scheduled posts more than 24 hours old.
if err := a.Srv().Store().ScheduledPost().UpdateOldScheduledPosts(beforeTime); err != nil {
rctx.Logger().Error(
"App.ProcessScheduledPosts: failed to update old scheduled posts",
mlog.Int("before_time", beforeTime),
mlog.Err(err),
)
}
}
// processScheduledPostBatch processes one batch
func (a *App) processScheduledPostBatch(rctx request.CTX, scheduledPosts []*model.ScheduledPost) error {
rctx.Logger().Debug("processScheduledPostBatch called...")
var failedScheduledPosts []*model.ScheduledPost
var successfulScheduledPostIDs []string
for i := range scheduledPosts {
rctx.Logger().Trace("processScheduledPostBatch processing scheduled post", mlog.String("scheduled_post_id", scheduledPosts[i].Id))
scheduledPost, err := a.postScheduledPost(rctx, scheduledPosts[i])
if err != nil {
rctx.Logger().Debug("processScheduledPostBatch scheduled post processing failed", mlog.String("scheduled_post_id", scheduledPosts[i].Id), mlog.Err(err))
failedScheduledPosts = append(failedScheduledPosts, scheduledPost)
continue
}
rctx.Logger().Trace("processScheduledPostBatch scheduled post processing successful", mlog.String("scheduled_post_id", scheduledPosts[i].Id))
successfulScheduledPostIDs = append(successfulScheduledPostIDs, scheduledPost.Id)
}
rctx.Logger().Trace("processScheduledPostBatch handling successful scheduled posts...", mlog.Int("count", len(successfulScheduledPostIDs)))
if err := a.handleSuccessfulScheduledPosts(rctx, successfulScheduledPostIDs); err != nil {
return errors.Wrap(err, "App.processScheduledPostBatch: failed to handle successfully posted scheduled posts")
}
rctx.Logger().Trace("processScheduledPostBatch handling failed scheduled posts...", mlog.Int("count", len(failedScheduledPosts)))
a.handleFailedScheduledPosts(rctx, failedScheduledPosts)
rctx.Logger().Debug("processScheduledPostBatch finished...")
return nil
}
// postScheduledPost processes an individual scheduled post
func (a *App) postScheduledPost(rctx request.CTX, scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
rctx.Logger().Debug("postScheduledPost called...", mlog.String("scheduled_post_id", scheduledPost.Id))
// we'll process scheduled posts one by one.
// If an error occurs, we'll log it and move onto the next scheduled post
rctx.Logger().Trace("postScheduledPost fetching channel for scheduled post", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.String("channel_id", scheduledPost.ChannelId))
channel, appErr := a.GetChannel(rctx, scheduledPost.ChannelId)
if appErr != nil {
if appErr.StatusCode == http.StatusNotFound {
rctx.Logger().Debug("postScheduledPost channel for scheduled post not found, setting error code", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.String("channel_id", scheduledPost.ChannelId), mlog.String("error_code", model.ScheduledPostErrorCodeChannelNotFound))
scheduledPost.ErrorCode = model.ScheduledPostErrorCodeChannelNotFound
return scheduledPost, nil
}
rctx.Logger().Error(
"App.processScheduledPostBatch: failed to get channel for scheduled post",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("channel_id", scheduledPost.ChannelId),
mlog.String("error_code", model.ScheduledPostErrorUnknownError),
mlog.Err(appErr),
)
scheduledPost.ErrorCode = model.ScheduledPostErrorUnknownError
return scheduledPost, appErr
}
rctx.Logger().Trace("postScheduledPost checking if scheduled post can be posted", mlog.String("scheduled_post_id", scheduledPost.Id))
errorCode, err := a.canPostScheduledPost(rctx, scheduledPost, channel)
scheduledPost.ErrorCode = errorCode
if err != nil {
rctx.Logger().Error(
"App.processScheduledPostBatch: failed to check if scheduled post can be posted",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("user_id", scheduledPost.UserId),
mlog.String("channel_id", scheduledPost.ChannelId),
mlog.Err(err),
)
return scheduledPost, err
}
if scheduledPost.ErrorCode != "" {
rctx.Logger().Warn(
"App.processScheduledPostBatch: skipping posting a scheduled post as `can post` check failed",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("user_id", scheduledPost.UserId),
mlog.String("channel_id", scheduledPost.ChannelId),
mlog.String("error_code", scheduledPost.ErrorCode),
)
return scheduledPost, fmt.Errorf("App.processScheduledPostBatch: skipping posting a scheduled post as `can post` check failed, error_code: %s", scheduledPost.ErrorCode)
}
rctx.Logger().Trace("postScheduledPost converting scheduled post to post", mlog.String("scheduled_post_id", scheduledPost.Id))
post, err := scheduledPost.ToPost()
if err != nil {
rctx.Logger().Error(
"App.processScheduledPostBatch: failed to convert scheduled post to a post",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("error_code", model.ScheduledPostErrorUnknownError),
mlog.Err(err),
)
scheduledPost.ErrorCode = model.ScheduledPostErrorUnknownError
return scheduledPost, err
}
rctx.Logger().Trace("postScheduledPost posting the scheduled post", mlog.String("scheduled_post_id", scheduledPost.Id))
createPostFlags := model.CreatePostFlags{
TriggerWebhooks: true,
SetOnline: false,
}
_, appErr = a.CreatePost(rctx, post, channel, createPostFlags)
if appErr != nil {
rctx.Logger().Error(
"App.processScheduledPostBatch: failed to post scheduled post",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("channel_id", scheduledPost.ChannelId),
mlog.String("error_code", model.ScheduledPostErrorUnknownError),
mlog.Err(appErr),
)
scheduledPost.ErrorCode = model.ScheduledPostErrorUnknownError
return scheduledPost, appErr
}
return scheduledPost, nil
}
// canPostScheduledPost checks whether the scheduled post be created based on permissions and other checks.
func (a *App) canPostScheduledPost(rctx request.CTX, scheduledPost *model.ScheduledPost, channel *model.Channel) (string, error) {
rctx.Logger().Trace("canPostScheduledPost called...", mlog.String("scheduled_post_id", scheduledPost.Id))
user, appErr := a.GetUser(scheduledPost.UserId)
if appErr != nil {
if appErr.Id == MissingAccountError {
rctx.Logger().Debug("canPostScheduledPost user not found for scheduled post", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.String("user_id", scheduledPost.UserId), mlog.String("error_code", model.ScheduledPostErrorCodeUserDoesNotExist))
return model.ScheduledPostErrorCodeUserDoesNotExist, nil
}
rctx.Logger().Error(
"App.canPostScheduledPost: failed to get user from database",
mlog.String("user_id", scheduledPost.UserId),
mlog.String("error_code", model.ScheduledPostErrorUnknownError),
mlog.Err(appErr),
)
return model.ScheduledPostErrorUnknownError, errors.Wrapf(appErr, "App.canPostScheduledPost: failed to get user from database, userId: %s", scheduledPost.UserId)
}
if user.DeleteAt != 0 {
rctx.Logger().Debug("canPostScheduledPost user for scheduled posts is deleted", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.String("user_id", scheduledPost.UserId), mlog.String("error_code", model.ScheduledPostErrorCodeUserDeleted))
return model.ScheduledPostErrorCodeUserDeleted, nil
}
if channel.DeleteAt != 0 {
rctx.Logger().Debug("canPostScheduledPost channel for scheduled post is archived", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.String("channel_id", channel.Id), mlog.String("error_code", model.ScheduledPostErrorCodeChannelArchived))
return model.ScheduledPostErrorCodeChannelArchived, nil
}
if scheduledPost.RootId != "" {
rootPosts, _, appErr := a.GetPostsByIds([]string{scheduledPost.RootId})
if appErr != nil {
if appErr.StatusCode == http.StatusNotFound {
rctx.Logger().Debug("canPostScheduledPost thread root post for scheduled post is missing", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.String("root_post_id", scheduledPost.RootId), mlog.String("error_code", model.ScheduledPostErrorThreadDeleted))
return model.ScheduledPostErrorThreadDeleted, nil
}
rctx.Logger().Error(
"App.canPostScheduledPost: failed to get root post",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("root_post_id", scheduledPost.RootId),
mlog.String("error_code", model.ScheduledPostErrorUnknownError),
mlog.Err(appErr),
)
return model.ScheduledPostErrorUnknownError, errors.Wrapf(appErr, "App.canPostScheduledPost: failed to get root post, scheduled_post_id: %s, root_post_id: %s", scheduledPost.Id, scheduledPost.RootId)
}
// you do get deleted posts from `GetPostsByIds`, so need to validate that as well
if len(rootPosts) == 1 && rootPosts[0].Id == scheduledPost.RootId && rootPosts[0].DeleteAt != 0 {
rctx.Logger().Debug("canPostScheduledPost thread root post is deleted", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.String("root_post_id", scheduledPost.RootId), mlog.String("error_code", model.ScheduledPostErrorThreadDeleted))
return model.ScheduledPostErrorThreadDeleted, nil
}
}
if appErr := userCreatePostPermissionCheckWithApp(rctx, a, scheduledPost.UserId, scheduledPost.ChannelId); appErr != nil {
rctx.Logger().Debug(
"canPostScheduledPost user does not have permission to create post in channel",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("user_id", scheduledPost.UserId),
mlog.String("channel_id", scheduledPost.ChannelId),
mlog.String("error_code", model.ScheduledPostErrorCodeNoChannelPermission),
mlog.Err(appErr),
)
return model.ScheduledPostErrorCodeNoChannelPermission, nil
}
if appErr := PostHardenedModeCheckWithApp(a, false, scheduledPost.GetProps()); appErr != nil {
rctx.Logger().Debug(
"canPostScheduledPost hardened mode enabled: post contains props prohibited in hardened mode",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("user_id", scheduledPost.UserId),
mlog.String("channel_id", scheduledPost.ChannelId),
mlog.String("error_code", model.ScheduledPostErrorInvalidPost),
mlog.Err(appErr),
)
return model.ScheduledPostErrorInvalidPost, nil
}
if appErr := PostPriorityCheckWithApp("ScheduledPostJob.postChecks", a, scheduledPost.UserId, scheduledPost.GetPriority(), scheduledPost.RootId); appErr != nil {
rctx.Logger().Debug(
"canPostScheduledPost post priority check failed",
mlog.String("scheduled_post_id", scheduledPost.Id),
mlog.String("user_id", scheduledPost.UserId),
mlog.String("channel_id", scheduledPost.ChannelId),
mlog.String("error_code", model.ScheduledPostErrorInvalidPost),
mlog.Err(appErr),
)
return model.ScheduledPostErrorInvalidPost, nil
}
rctx.Logger().Debug("canPostScheduledPost scheduled post can be posted", mlog.String("scheduled_post_id", scheduledPost.Id))
return "", nil
}
func (a *App) handleSuccessfulScheduledPosts(rctx request.CTX, successfulScheduledPostIDs []string) error {
if len(successfulScheduledPostIDs) > 0 {
// Successfully posted scheduled posts can be safely permanently deleted as no data is lost.
// The data is moved into the posts table.
err := a.Srv().Store().ScheduledPost().PermanentlyDeleteScheduledPosts(successfulScheduledPostIDs)
if err != nil {
rctx.Logger().Error(
"App.handleSuccessfulScheduledPosts: failed to delete successfully posted scheduled posts",
mlog.Int("successfully_posted_count", len(successfulScheduledPostIDs)),
mlog.Err(err),
)
return errors.Wrap(err, "App.handleSuccessfulScheduledPosts: failed to delete successfully posted scheduled posts")
}
}
return nil
}
func (a *App) handleFailedScheduledPosts(rctx request.CTX, failedScheduledPosts []*model.ScheduledPost) {
for _, failedScheduledPost := range failedScheduledPosts {
err := a.Srv().Store().ScheduledPost().UpdatedScheduledPost(failedScheduledPost)
if err != nil {
// we intentionally don't stop on error as its possible to continue updating other scheduled posts
rctx.Logger().Error(
"App.processScheduledPostBatch: failed to updated failed scheduled posts",
mlog.String("scheduled_post_id", failedScheduledPost.Id),
mlog.Err(err),
)
}
}
}

Просмотреть файл

@@ -0,0 +1,211 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/assert"
)
func TestProcessScheduledPosts(t *testing.T) {
t.Run("base case - happy path", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
scheduledAt := model.GetMillis() + 1000
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err := th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost1)
assert.NoError(t, err)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is second scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err = th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost2)
assert.NoError(t, err)
time.Sleep(1 * time.Second)
th.App.ProcessScheduledPosts(th.Context)
scheduledPosts, err := th.App.Srv().Store().ScheduledPost().GetScheduledPostsForUser(th.BasicUser.Id, th.BasicChannel.TeamId)
assert.NoError(t, err)
assert.Len(t, scheduledPosts, 0)
})
t.Run("sets error code for archived channel", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
appErr := th.App.DeleteChannel(th.Context, th.BasicChannel, th.BasicUser.Id)
assert.Nil(t, appErr)
scheduledAt := model.GetMillis() - (5 * 60 * 60 * 1000)
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err := th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost1)
assert.NoError(t, err)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is second scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err = th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost2)
assert.NoError(t, err)
time.Sleep(1 * time.Second)
th.App.ProcessScheduledPosts(th.Context)
// since the channel ID we set in the above created scheduled posts is of a
// non-existing channel, the job should have set the appropriate error code for them in the database
scheduledPosts, err := th.App.Srv().Store().ScheduledPost().GetScheduledPostsForUser(th.BasicUser.Id, th.BasicChannel.TeamId)
assert.NoError(t, err)
assert.Len(t, scheduledPosts, 2)
assert.Equal(t, model.ScheduledPostErrorCodeChannelArchived, scheduledPosts[0].ErrorCode)
assert.Greater(t, scheduledPosts[0].ProcessedAt, int64(0))
assert.Equal(t, model.ScheduledPostErrorCodeChannelArchived, scheduledPosts[1].ErrorCode)
assert.Greater(t, scheduledPosts[1].ProcessedAt, int64(0))
})
t.Run("sets error code for archived user", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
scheduledAt := model.GetMillis() + 1000
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err := th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost1)
assert.NoError(t, err)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is second scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err = th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost2)
assert.NoError(t, err)
_, appErr := th.App.UpdateActive(th.Context, th.BasicUser, false)
assert.Nil(t, appErr)
defer func() {
_, _ = th.App.UpdateActive(th.Context, th.BasicUser, true)
}()
time.Sleep(1 * time.Second)
th.App.ProcessScheduledPosts(th.Context)
scheduledPosts, err := th.App.Srv().Store().ScheduledPost().GetScheduledPostsForUser(th.BasicUser.Id, th.BasicChannel.TeamId)
assert.NoError(t, err)
assert.Len(t, scheduledPosts, 2)
assert.Equal(t, model.ScheduledPostErrorCodeUserDeleted, scheduledPosts[0].ErrorCode)
assert.Greater(t, scheduledPosts[0].ProcessedAt, int64(0))
assert.Equal(t, model.ScheduledPostErrorCodeUserDeleted, scheduledPosts[1].ErrorCode)
assert.Greater(t, scheduledPosts[1].ProcessedAt, int64(0))
})
t.Run("sets error code when user is not a channel member", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
scheduledAt := model.GetMillis() + 1000
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err := th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost1)
assert.NoError(t, err)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is second scheduled post",
},
ScheduledAt: scheduledAt,
}
_, err = th.Server.Store().ScheduledPost().CreateScheduledPost(scheduledPost2)
assert.NoError(t, err)
appErr := th.App.LeaveChannel(th.Context, th.BasicChannel.Id, th.BasicUser.Id)
assert.Nil(t, appErr)
defer func() {
_ = th.App.JoinChannel(th.Context, th.BasicChannel, th.BasicUser.Id)
}()
time.Sleep(1 * time.Second)
th.App.ProcessScheduledPosts(th.Context)
scheduledPosts, err := th.App.Srv().Store().ScheduledPost().GetScheduledPostsForUser(th.BasicUser.Id, th.BasicChannel.TeamId)
assert.NoError(t, err)
assert.Len(t, scheduledPosts, 2)
assert.Equal(t, model.ScheduledPostErrorCodeNoChannelPermission, scheduledPosts[0].ErrorCode)
assert.Greater(t, scheduledPosts[0].ProcessedAt, int64(0))
assert.Equal(t, model.ScheduledPostErrorCodeNoChannelPermission, scheduledPosts[1].ErrorCode)
assert.Greater(t, scheduledPosts[1].ProcessedAt, int64(0))
})
}

663
server/channels/app/scheduled_post_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,663 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"testing"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/require"
)
func TestSaveScheduledPost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("base case", func(t *testing.T) {
userId := model.NewId()
channel, err := th.GetSqlStore().Channel().Save(th.Context, &model.Channel{
Name: model.NewId(),
DisplayName: "Channel",
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, err)
_, err = th.GetSqlStore().Channel().SaveMember(th.Context, &model.ChannelMember{
ChannelId: channel.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
})
require.NoError(t, err)
defer func() {
_ = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
_ = th.GetSqlStore().Channel().RemoveMember(th.Context, channel.Id, userId)
}()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
})
t.Run("cannot save invalid scheduled post", func(t *testing.T) {
scheduledPost := &model.ScheduledPost{
// a completely empty scheduled post
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.NotNil(t, appErr)
require.Nil(t, createdScheduledPost)
})
t.Run("cannot save post scheduled in the past", func(t *testing.T) {
userId := model.NewId()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: "channel_id",
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() - 100000, // 100 seconds in the past
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.NotNil(t, appErr)
require.Nil(t, createdScheduledPost)
})
t.Run("cannot scheduled post in a channel you don't belong to", func(t *testing.T) {
userId := model.NewId()
// we didn't create any channel member entry, so the user doesn't
// belong to the channel
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: "channel_id",
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.NotNil(t, appErr)
require.Nil(t, createdScheduledPost)
})
t.Run("cannot schedule post in an archived channel", func(t *testing.T) {
userId := model.NewId()
channel, err := th.GetSqlStore().Channel().Save(th.Context, &model.Channel{
Name: model.NewId(),
DisplayName: "Channel",
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, err)
_, err = th.GetSqlStore().Channel().SaveMember(th.Context, &model.ChannelMember{
ChannelId: channel.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
})
require.NoError(t, err)
err = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
require.NoError(t, err)
defer func() {
_ = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
_ = th.GetSqlStore().Channel().RemoveMember(th.Context, channel.Id, userId)
}()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.NotNil(t, appErr)
require.Nil(t, createdScheduledPost)
})
t.Run("can scheduled multiple posts in the same channel", func(t *testing.T) {
userId := model.NewId()
channel, err := th.GetSqlStore().Channel().Save(th.Context, &model.Channel{
Name: model.NewId(),
DisplayName: "Channel",
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, err)
_, err = th.GetSqlStore().Channel().SaveMember(th.Context, &model.ChannelMember{
ChannelId: channel.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
})
require.NoError(t, err)
defer func() {
_ = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
_ = th.GetSqlStore().Channel().RemoveMember(th.Context, channel.Id, userId)
}()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
scheduledPost.Message = "this is a second scheduled post"
scheduledPost.Id = model.NewId()
createdScheduledPost, appErr = th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
})
t.Run("cannot save an empty post", func(t *testing.T) {
userId := model.NewId()
channel, err := th.GetSqlStore().Channel().Save(th.Context, &model.Channel{
Name: model.NewId(),
DisplayName: "Channel",
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, err)
_, err = th.GetSqlStore().Channel().SaveMember(th.Context, &model.ChannelMember{
ChannelId: channel.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
})
require.NoError(t, err)
defer func() {
_ = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
_ = th.GetSqlStore().Channel().RemoveMember(th.Context, channel.Id, userId)
}()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel.Id,
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.NotNil(t, appErr)
require.Nil(t, createdScheduledPost)
})
}
func TestGetUserTeamScheduledPosts(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("should get created scheduled posts", func(t *testing.T) {
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost1, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost1)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost1)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a second scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost2, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost2)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost2)
defer func() {
_ = th.Server.Store().ScheduledPost().PermanentlyDeleteScheduledPosts([]string{scheduledPost1.Id, createdScheduledPost2.Id})
}()
retrievedScheduledPosts, appErr := th.App.GetUserTeamScheduledPosts(th.Context, th.BasicUser.Id, th.BasicChannel.TeamId)
require.Nil(t, appErr)
require.Equal(t, 2, len(retrievedScheduledPosts))
// more recently created scheduled post appears first in the list
require.Equal(t, createdScheduledPost1.Id, retrievedScheduledPosts[0].Id)
require.Equal(t, createdScheduledPost2.Id, retrievedScheduledPosts[1].Id)
})
t.Run("should handle no scheduled posts", func(t *testing.T) {
retrievedScheduledPosts, appErr := th.App.GetUserTeamScheduledPosts(th.Context, th.BasicUser.Id, th.BasicChannel.TeamId)
require.Nil(t, appErr)
require.Equal(t, 0, len(retrievedScheduledPosts))
})
t.Run("should restrict to specified teams and DM/GMs", func(t *testing.T) {
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost1, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost1)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost1)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a second scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost2, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost2)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost2)
defer func() {
_ = th.Server.Store().ScheduledPost().PermanentlyDeleteScheduledPosts([]string{scheduledPost1.Id, createdScheduledPost2.Id})
}()
// create a dummy team
secondTeam := th.CreateTeam()
_, appErr = th.App.JoinUserToTeam(th.Context, secondTeam, th.BasicUser, th.BasicUser.Id)
require.Nil(t, appErr)
retrievedScheduledPosts, appErr := th.App.GetUserTeamScheduledPosts(th.Context, th.BasicUser.Id, secondTeam.Id)
require.Nil(t, appErr)
require.Equal(t, 0, len(retrievedScheduledPosts))
})
t.Run("should not return scheduled posts from DMs and GMs when teamId is specified", func(t *testing.T) {
// start a DM between BasicUser1 and BasicUser2
dm, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
require.Nil(t, appErr)
// create a GM. Since a GM needs at least 3 users, we'll create a third user first
thirdUser := th.CreateUser()
_, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, thirdUser, thirdUser.Id)
require.Nil(t, appErr)
gm, appErr := th.App.CreateGroupChannel(th.Context, []string{th.BasicUser.Id, th.BasicUser2.Id, thirdUser.Id}, th.BasicUser.Id)
require.Nil(t, appErr)
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: dm.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost1, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost1)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost1)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: gm.Id,
Message: "this is a second scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost2, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost2)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost2)
defer func() {
_ = th.Server.Store().ScheduledPost().PermanentlyDeleteScheduledPosts([]string{scheduledPost1.Id, createdScheduledPost2.Id})
}()
retrievedScheduledPosts, appErr := th.App.GetUserTeamScheduledPosts(th.Context, th.BasicUser.Id, th.BasicChannel.TeamId)
require.Nil(t, appErr)
require.Equal(t, 0, len(retrievedScheduledPosts))
})
t.Run("should return scheduled posts from DMs and GMs when teamId is empty", func(t *testing.T) {
// start a DM between BasicUser1 and BasicUser2
dm, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
require.Nil(t, appErr)
// create a GM. Since a GM needs at least 3 users, we'll create a third user first
thirdUser := th.CreateUser()
_, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, thirdUser, thirdUser.Id)
require.Nil(t, appErr)
gm, appErr := th.App.CreateGroupChannel(th.Context, []string{th.BasicUser.Id, th.BasicUser2.Id, thirdUser.Id}, th.BasicUser.Id)
require.Nil(t, appErr)
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: dm.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost1, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost1)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost1)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis() + 100,
UserId: th.BasicUser.Id,
ChannelId: gm.Id,
Message: "this is a second scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost2, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost2)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost2)
defer func() {
_ = th.Server.Store().ScheduledPost().PermanentlyDeleteScheduledPosts([]string{scheduledPost1.Id, createdScheduledPost2.Id})
}()
retrievedScheduledPosts, appErr := th.App.GetUserTeamScheduledPosts(th.Context, th.BasicUser.Id, "")
require.Nil(t, appErr)
require.Equal(t, 2, len(retrievedScheduledPosts))
// more recently created scheduled post appears first in the list
require.Equal(t, createdScheduledPost1.Id, retrievedScheduledPosts[0].Id)
require.Equal(t, createdScheduledPost2.Id, retrievedScheduledPosts[1].Id)
})
}
func TestUpdateScheduledPost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("base case", func(t *testing.T) {
// first we'll create a scheduled post
userId := model.NewId()
channel, err := th.GetSqlStore().Channel().Save(th.Context, &model.Channel{
Name: model.NewId(),
DisplayName: "Channel",
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, err)
_, err = th.GetSqlStore().Channel().SaveMember(th.Context, &model.ChannelMember{
ChannelId: channel.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
})
require.NoError(t, err)
defer func() {
_ = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
_ = th.GetSqlStore().Channel().RemoveMember(th.Context, channel.Id, userId)
}()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
// now we'll try updating it
newScheduledAtTime := model.GetMillis() + 9999999
createdScheduledPost.ScheduledAt = newScheduledAtTime
createdScheduledPost.Message = "Updated Message!!!"
updatedScheduledPost, appErr := th.App.UpdateScheduledPost(th.Context, userId, createdScheduledPost)
require.Nil(t, appErr)
require.NotNil(t, updatedScheduledPost)
require.Equal(t, newScheduledAtTime, updatedScheduledPost.ScheduledAt)
require.Equal(t, "Updated Message!!!", updatedScheduledPost.Message)
})
t.Run("should ot be allowed to updated a scheduled post not belonging to the user", func(t *testing.T) {
// first we'll create a scheduled post
userId := model.NewId()
channel, err := th.GetSqlStore().Channel().Save(th.Context, &model.Channel{
Name: model.NewId(),
DisplayName: "Channel",
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, err)
_, err = th.GetSqlStore().Channel().SaveMember(th.Context, &model.ChannelMember{
ChannelId: channel.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
})
require.NoError(t, err)
defer func() {
_ = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
_ = th.GetSqlStore().Channel().RemoveMember(th.Context, channel.Id, userId)
}()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
// now we'll try updating it
newScheduledAtTime := model.GetMillis() + 9999999
createdScheduledPost.ScheduledAt = newScheduledAtTime
createdScheduledPost.Message = "Updated Message!!!"
updatedScheduledPost, appErr := th.App.UpdateScheduledPost(th.Context, th.BasicUser2.Id, createdScheduledPost)
require.NotNil(t, appErr)
require.Equal(t, http.StatusForbidden, appErr.StatusCode)
require.Nil(t, updatedScheduledPost)
})
t.Run("should only allow updating limited fields", func(t *testing.T) {
// first we'll create a scheduled post
userId := model.NewId()
channel, err := th.GetSqlStore().Channel().Save(th.Context, &model.Channel{
Name: model.NewId(),
DisplayName: "Channel",
Type: model.ChannelTypeOpen,
}, 1000)
require.NoError(t, err)
_, err = th.GetSqlStore().Channel().SaveMember(th.Context, &model.ChannelMember{
ChannelId: channel.Id,
UserId: userId,
NotifyProps: model.GetDefaultChannelNotifyProps(),
SchemeGuest: false,
SchemeUser: true,
})
require.NoError(t, err)
defer func() {
_ = th.GetSqlStore().Channel().Delete(channel.Id, model.GetMillis())
_ = th.GetSqlStore().Channel().RemoveMember(th.Context, channel.Id, userId)
}()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: channel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
// now we'll try updating it
newUpdatedAt := model.GetMillis() + 1000000
createdScheduledPost.UpdateAt = newUpdatedAt // this should be overridden by the actual update time
createdScheduledPost.Message = "Updated Message" // this will update
newChannelId := model.NewId()
createdScheduledPost.ChannelId = newChannelId // this won't update
newCreateAt := model.GetMillis() + 5000000
createdScheduledPost.CreateAt = newCreateAt // this won't update
createdScheduledPost.FileIds = []string{model.NewId(), model.NewId()}
createdScheduledPost.ErrorCode = model.ScheduledPostErrorUnknownError
updatedScheduledPost, appErr := th.App.UpdateScheduledPost(th.Context, userId, createdScheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
require.NotEqual(t, newUpdatedAt, updatedScheduledPost.UpdateAt)
require.Equal(t, "Updated Message", updatedScheduledPost.Message)
require.NotEqual(t, newChannelId, updatedScheduledPost.ChannelId)
require.NotEqual(t, newCreateAt, updatedScheduledPost.CreateAt)
require.Equal(t, 2, len(updatedScheduledPost.FileIds))
require.Equal(t, model.ScheduledPostErrorUnknownError, createdScheduledPost.ErrorCode)
})
}
func TestDeleteScheduledPost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("base case", func(t *testing.T) {
// first we'll create a scheduled post
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
fetchedScheduledPost, err := th.Server.Store().ScheduledPost().Get(scheduledPost.Id)
require.NoError(t, err)
require.NotNil(t, fetchedScheduledPost)
require.Equal(t, createdScheduledPost.Id, fetchedScheduledPost.Id)
require.Equal(t, createdScheduledPost.Message, fetchedScheduledPost.Message)
// now we'll delete it
var deletedScheduledPost *model.ScheduledPost
deletedScheduledPost, appErr = th.App.DeleteScheduledPost(th.Context, th.BasicUser.Id, scheduledPost.Id, "connection_id")
require.Nil(t, appErr)
require.NotNil(t, deletedScheduledPost)
require.Equal(t, scheduledPost.Id, deletedScheduledPost.Id)
require.Equal(t, scheduledPost.Message, deletedScheduledPost.Message)
// try to fetch it again
reFetchedScheduledPost, err := th.Server.Store().ScheduledPost().Get(scheduledPost.Id)
require.Error(t, err) // This will produce error as the row doesn't exist
require.Nil(t, reFetchedScheduledPost)
})
t.Run("should not allow deleting someone else's scheduled post", func(t *testing.T) {
// first we'll create a scheduled post
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, appErr := th.App.SaveScheduledPost(th.Context, scheduledPost)
require.Nil(t, appErr)
require.NotNil(t, createdScheduledPost)
fetchedScheduledPost, err := th.Server.Store().ScheduledPost().Get(scheduledPost.Id)
require.NoError(t, err)
require.NotNil(t, fetchedScheduledPost)
require.Equal(t, createdScheduledPost.Id, fetchedScheduledPost.Id)
require.Equal(t, createdScheduledPost.Message, fetchedScheduledPost.Message)
// now we'll delete it
var deletedScheduledPost *model.ScheduledPost
deletedScheduledPost, appErr = th.App.DeleteScheduledPost(th.Context, th.BasicUser2.Id, scheduledPost.Id, "connection_id")
require.NotNil(t, appErr)
require.Nil(t, deletedScheduledPost)
// try to fetch it again
reFetchedScheduledPost, err := th.Server.Store().ScheduledPost().Get(scheduledPost.Id)
require.NoError(t, err)
require.NotNil(t, reFetchedScheduledPost)
require.Equal(t, createdScheduledPost.Id, reFetchedScheduledPost.Id)
require.Equal(t, createdScheduledPost.Message, reFetchedScheduledPost.Message)
})
t.Run("should producer error when deleting non existing scheduled post", func(t *testing.T) {
var deletedScheduledPost *model.ScheduledPost
deletedScheduledPost, appErr := th.App.DeleteScheduledPost(th.Context, th.BasicUser.Id, model.NewId(), "connection_id")
require.NotNil(t, appErr)
require.Nil(t, deletedScheduledPost)
})
}

Просмотреть файл

@@ -81,6 +81,10 @@ import (
"github.com/mattermost/mattermost/server/v8/platform/shared/templates"
)
const (
scheduledPostJobInterval = 5 * time.Minute
)
var SentryDSN = "https://9d7c9cccf549479799f880bcf4f26323@o94110.ingest.sentry.io/5212327"
// This is a placeholder to allow the existing release pipelines to run without failing to insert
@@ -506,6 +510,7 @@ func (s *Server) runJobs() {
appInstance := New(ServerConnector(s.Channels()))
runDNDStatusExpireJob(appInstance)
runPostReminderJob(appInstance)
runScheduledPostJob(appInstance)
})
s.Go(func() {
runSecurityJob(s)
@@ -1819,6 +1824,30 @@ func runPostReminderJob(a *App) {
})
}
func runScheduledPostJob(a *App) {
if a.IsLeader() {
doRunScheduledPostJob(a)
}
a.ch.srv.AddClusterLeaderChangedListener(func() {
mlog.Info("Cluster leader changed. Determining if scheduled posts task should be running", mlog.Bool("isLeader", a.IsLeader()))
if a.IsLeader() {
doRunScheduledPostJob(a)
} else {
mlog.Info("This is no longer leader node. Cancelling the scheduled post task", mlog.Bool("isLeader", a.IsLeader()))
cancelTask(&a.ch.scheduledPostMut, &a.ch.scheduledPostTask)
}
})
}
func doRunScheduledPostJob(a *App) {
rctx := request.EmptyContext(a.Log())
withMut(&a.ch.scheduledPostMut, func() {
fn := func() { a.ProcessScheduledPosts(rctx) }
a.ch.scheduledPostTask = model.CreateRecurringTaskFromNextIntervalTime("Process Scheduled Posts", fn, scheduledPostJobInterval)
})
}
func (a *App) GetAppliedSchemaMigrations() ([]model.AppliedMigration, *model.AppError) {
table, err := a.Srv().Store().GetAppliedMigrations()
if err != nil {

Просмотреть файл

@@ -251,6 +251,8 @@ channels/db/migrations/mysql/000126_sharedchannels_remotes_add_deleteat.down.sql
channels/db/migrations/mysql/000126_sharedchannels_remotes_add_deleteat.up.sql
channels/db/migrations/mysql/000127_add_mfa_used_ts_to_users.down.sql
channels/db/migrations/mysql/000127_add_mfa_used_ts_to_users.up.sql
channels/db/migrations/mysql/000128_create_scheduled_posts.down.sql
channels/db/migrations/mysql/000128_create_scheduled_posts.up.sql
channels/db/migrations/postgres/000001_create_teams.down.sql
channels/db/migrations/postgres/000001_create_teams.up.sql
channels/db/migrations/postgres/000002_create_team_members.down.sql
@@ -503,3 +505,5 @@ channels/db/migrations/postgres/000126_sharedchannels_remotes_add_deleteat.down.
channels/db/migrations/postgres/000126_sharedchannels_remotes_add_deleteat.up.sql
channels/db/migrations/postgres/000127_add_mfa_used_ts_to_users.down.sql
channels/db/migrations/postgres/000127_add_mfa_used_ts_to_users.up.sql
channels/db/migrations/postgres/000128_create_scheduled_posts.down.sql
channels/db/migrations/postgres/000128_create_scheduled_posts.up.sql

Просмотреть файл

@@ -0,0 +1,15 @@
DROP TABLE IF EXISTS scheduledposts;
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = 'ScheduledPosts'
AND table_schema = DATABASE()
AND index_name = 'idx_scheduledposts_userid_channel_id_scheduled_at'
) > 0,
'DROP INDEX idx_scheduledposts_userid_channel_id_scheduled_at on ScheduledPosts;',
'SELECT 1;'
));
PREPARE removeIndexIfExists FROM @preparedStatement;
EXECUTE removeIndexIfExists;
DEALLOCATE PREPARE removeIndexIfExists;

Просмотреть файл

@@ -0,0 +1,29 @@
CREATE TABLE IF NOT EXISTS ScheduledPosts (
id VARCHAR(26) PRIMARY KEY,
createat bigint(20),
updateat bigint(20),
userid VARCHAR(26) NOT NULL,
channelid VARCHAR(26) NOT NULL,
rootid VARCHAR(26),
message text,
props text,
fileids text,
priority text,
scheduledat bigint(20) NOT NULL,
processedat bigint(20),
errorcode VARCHAR(200)
);
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = 'ScheduledPosts'
AND table_schema = DATABASE()
AND index_name = 'idx_scheduledposts_userid_channel_id_scheduled_at'
) > 0,
'SELECT 1',
'CREATE INDEX idx_scheduledposts_userid_channel_id_scheduled_at ON ScheduledPosts (UserId, ChannelId, ScheduledAt);'
));
PREPARE createIndexIfNotExists FROM @preparedStatement;
EXECUTE createIndexIfNotExists;
DEALLOCATE PREPARE createIndexIfNotExists;

Просмотреть файл

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS scheduledposts;
DROP INDEX IF EXISTS idx_scheduledposts_userid_channel_id_scheduled_at;

Просмотреть файл

@@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS scheduledposts (
id VARCHAR(26) PRIMARY KEY,
createat bigint,
updateat bigint,
userid VARCHAR(26) NOT NULL,
channelid VARCHAR(26) NOT NULL,
rootid VARCHAR(26),
message VARCHAR(65535),
props VARCHAR(8000),
fileids VARCHAR(300),
priority text,
scheduledat bigint NOT NULL,
processedat bigint,
errorcode VARCHAR(200)
);
CREATE INDEX IF NOT EXISTS idx_scheduledposts_userid_channel_id_scheduled_at ON ScheduledPosts (UserId, ChannelId, ScheduledAt);

Просмотреть файл

@@ -50,6 +50,7 @@ type OpenTracingLayer struct {
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
ScheduledPostStore store.ScheduledPostStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
@@ -190,6 +191,10 @@ func (s *OpenTracingLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *OpenTracingLayer) ScheduledPost() store.ScheduledPostStore {
return s.ScheduledPostStore
}
func (s *OpenTracingLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
@@ -401,6 +406,11 @@ type OpenTracingLayerRoleStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerScheduledPostStore struct {
store.ScheduledPostStore
Root *OpenTracingLayer
}
type OpenTracingLayerSchemeStore struct {
store.SchemeStore
Root *OpenTracingLayer
@@ -8602,6 +8612,145 @@ func (s *OpenTracingLayerRoleStore) Save(role *model.Role) (*model.Role, error)
return result, err
}
func (s *OpenTracingLayerScheduledPostStore) CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.CreateScheduledPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ScheduledPostStore.CreateScheduledPost(scheduledPost)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerScheduledPostStore) Get(scheduledPostId string) (*model.ScheduledPost, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ScheduledPostStore.Get(scheduledPostId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerScheduledPostStore) GetMaxMessageSize() int {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.GetMaxMessageSize")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result := s.ScheduledPostStore.GetMaxMessageSize()
return result
}
func (s *OpenTracingLayerScheduledPostStore) GetPendingScheduledPosts(beforeTime int64, afterTime int64, lastScheduledPostId string, perPage uint64) ([]*model.ScheduledPost, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.GetPendingScheduledPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ScheduledPostStore.GetPendingScheduledPosts(beforeTime, afterTime, lastScheduledPostId, perPage)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerScheduledPostStore) GetScheduledPostsForUser(userId string, teamId string) ([]*model.ScheduledPost, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.GetScheduledPostsForUser")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ScheduledPostStore.GetScheduledPostsForUser(userId, teamId)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerScheduledPostStore) PermanentlyDeleteScheduledPosts(scheduledPostIDs []string) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.PermanentlyDeleteScheduledPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ScheduledPostStore.PermanentlyDeleteScheduledPosts(scheduledPostIDs)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerScheduledPostStore) UpdateOldScheduledPosts(beforeTime int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.UpdateOldScheduledPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ScheduledPostStore.UpdateOldScheduledPosts(beforeTime)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerScheduledPostStore) UpdatedScheduledPost(scheduledPost *model.ScheduledPost) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ScheduledPostStore.UpdatedScheduledPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.ScheduledPostStore.UpdatedScheduledPost(scheduledPost)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerSchemeStore) CountByScope(scope string) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SchemeStore.CountByScope")
@@ -13672,6 +13821,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.RemoteClusterStore = &OpenTracingLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &OpenTracingLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &OpenTracingLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.ScheduledPostStore = &OpenTracingLayerScheduledPostStore{ScheduledPostStore: childStore.ScheduledPost(), Root: &newStore}
newStore.SchemeStore = &OpenTracingLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &OpenTracingLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &OpenTracingLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}

Просмотреть файл

@@ -54,6 +54,7 @@ type RetryLayer struct {
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
ScheduledPostStore store.ScheduledPostStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
@@ -194,6 +195,10 @@ func (s *RetryLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *RetryLayer) ScheduledPost() store.ScheduledPostStore {
return s.ScheduledPostStore
}
func (s *RetryLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
@@ -405,6 +410,11 @@ type RetryLayerRoleStore struct {
Root *RetryLayer
}
type RetryLayerScheduledPostStore struct {
store.ScheduledPostStore
Root *RetryLayer
}
type RetryLayerSchemeStore struct {
store.SchemeStore
Root *RetryLayer
@@ -9812,6 +9822,159 @@ func (s *RetryLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
}
func (s *RetryLayerScheduledPostStore) CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
tries := 0
for {
result, err := s.ScheduledPostStore.CreateScheduledPost(scheduledPost)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerScheduledPostStore) Get(scheduledPostId string) (*model.ScheduledPost, error) {
tries := 0
for {
result, err := s.ScheduledPostStore.Get(scheduledPostId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerScheduledPostStore) GetMaxMessageSize() int {
return s.ScheduledPostStore.GetMaxMessageSize()
}
func (s *RetryLayerScheduledPostStore) GetPendingScheduledPosts(beforeTime int64, afterTime int64, lastScheduledPostId string, perPage uint64) ([]*model.ScheduledPost, error) {
tries := 0
for {
result, err := s.ScheduledPostStore.GetPendingScheduledPosts(beforeTime, afterTime, lastScheduledPostId, perPage)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerScheduledPostStore) GetScheduledPostsForUser(userId string, teamId string) ([]*model.ScheduledPost, error) {
tries := 0
for {
result, err := s.ScheduledPostStore.GetScheduledPostsForUser(userId, teamId)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerScheduledPostStore) PermanentlyDeleteScheduledPosts(scheduledPostIDs []string) error {
tries := 0
for {
err := s.ScheduledPostStore.PermanentlyDeleteScheduledPosts(scheduledPostIDs)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerScheduledPostStore) UpdateOldScheduledPosts(beforeTime int64) error {
tries := 0
for {
err := s.ScheduledPostStore.UpdateOldScheduledPosts(beforeTime)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerScheduledPostStore) UpdatedScheduledPost(scheduledPost *model.ScheduledPost) error {
tries := 0
for {
err := s.ScheduledPostStore.UpdatedScheduledPost(scheduledPost)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSchemeStore) CountByScope(scope string) (int64, error) {
tries := 0
@@ -15605,6 +15768,7 @@ func New(childStore store.Store) *RetryLayer {
newStore.RemoteClusterStore = &RetryLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &RetryLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &RetryLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.ScheduledPostStore = &RetryLayerScheduledPostStore{ScheduledPostStore: childStore.ScheduledPost(), Root: &newStore}
newStore.SchemeStore = &RetryLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &RetryLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &RetryLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}

Просмотреть файл

@@ -62,6 +62,7 @@ func genStore() *mocks.Store {
mock.On("PostPersistentNotification").Return(&mocks.PostPersistentNotificationStore{})
mock.On("DesktopTokens").Return(&mocks.DesktopTokensStore{})
mock.On("ChannelBookmark").Return(&mocks.ChannelBookmarkStore{})
mock.On("ScheduledPost").Return(&mocks.ScheduledPostStore{})
return mock
}

Просмотреть файл

@@ -0,0 +1,289 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"strings"
"sync"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
)
type SqlScheduledPostStore struct {
*SqlStore
maxMessageSizeOnce sync.Once
maxMessageSizeCached int
}
func newScheduledPostStore(sqlStore *SqlStore) *SqlScheduledPostStore {
return &SqlScheduledPostStore{
SqlStore: sqlStore,
maxMessageSizeCached: model.PostMessageMaxRunesV2,
}
}
func (s *SqlScheduledPostStore) columns(prefix string) []string {
if prefix != "" && !strings.HasSuffix(prefix, ".") {
prefix = prefix + "."
}
return []string{
prefix + "Id",
prefix + "CreateAt",
prefix + "UpdateAt",
prefix + "UserId",
prefix + "ChannelId",
prefix + "RootId",
prefix + "Message",
prefix + "Props",
prefix + "FileIds",
prefix + "Priority",
prefix + "ScheduledAt",
prefix + "ProcessedAt",
prefix + "ErrorCode",
}
}
func (s *SqlScheduledPostStore) scheduledPostToSlice(scheduledPost *model.ScheduledPost) []interface{} {
return []interface{}{
scheduledPost.Id,
scheduledPost.CreateAt,
scheduledPost.UpdateAt,
scheduledPost.UserId,
scheduledPost.ChannelId,
scheduledPost.RootId,
scheduledPost.Message,
model.StringInterfaceToJSON(scheduledPost.GetProps()),
model.ArrayToJSON(scheduledPost.FileIds),
model.StringInterfaceToJSON(scheduledPost.Priority),
scheduledPost.ScheduledAt,
scheduledPost.ProcessedAt,
scheduledPost.ErrorCode,
}
}
func (s *SqlScheduledPostStore) CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
scheduledPost.PreSave()
builder := s.getQueryBuilder().
Insert("ScheduledPosts").
Columns(s.columns("")...).
Values(s.scheduledPostToSlice(scheduledPost)...)
query, args, err := builder.ToSql()
if err != nil {
mlog.Error("SqlScheduledPostStore.CreateScheduledPost failed to generate SQL from query builder", mlog.Err(err))
return nil, errors.Wrap(err, "SqlScheduledPostStore.CreateScheduledPost failed to generate SQL from query builder")
}
if _, err := s.GetMasterX().Exec(query, args...); err != nil {
mlog.Error("SqlScheduledPostStore.CreateScheduledPost failed to insert scheduled post", mlog.Err(err))
return nil, errors.Wrap(err, "SqlScheduledPostStore.CreateScheduledPost failed to insert scheduled post")
}
return scheduledPost, nil
}
func (s *SqlScheduledPostStore) GetScheduledPostsForUser(userId, teamId string) ([]*model.ScheduledPost, error) {
// return scheduled posts for this user for
// specified team.
//
//An empty teamId fetches scheduled posts belonging to
// DMs and GMs (DMs and GMs do not belong to any team
// We're intentionally including scheduled posts from archived channels,
// or channels the user no longer belongs to as we want to still show those
// scheduled posts with appropriate error to the user.
// This is why we're not joining with ChannelMembers, and directly
// joining with Channels table.
query := s.getQueryBuilder().
Select(s.columns("sp")...).
From("ScheduledPosts AS sp").
InnerJoin("Channels as c on sp.ChannelId = c.Id").
Where(sq.Eq{
"sp.UserId": userId,
"c.TeamId": teamId,
}).
OrderBy("sp.ScheduledAt, sp.CreateAt")
var scheduledPosts []*model.ScheduledPost
if err := s.GetReplicaX().SelectBuilder(&scheduledPosts, query); err != nil {
mlog.Error("SqlScheduledPostStore.GetScheduledPostsForUser: failed to fetch scheduled posts for user", mlog.String("user_id", userId), mlog.String("team_id", teamId), mlog.Err(err))
return nil, errors.Wrapf(err, "SqlScheduledPostStore.GetScheduledPostsForUser: failed to fetch scheduled posts for user, userId: %s, teamID: %s", userId, teamId)
}
return scheduledPosts, nil
}
func (s *SqlScheduledPostStore) GetMaxMessageSize() int {
s.maxMessageSizeOnce.Do(func() {
var err error
s.maxMessageSizeCached, err = s.SqlStore.determineMaxColumnSize("ScheduledPosts", "Message")
if err != nil {
mlog.Error("SqlScheduledPostStore.getMaxMessageSize: error occurred during determining max column size for ScheduledPosts.Message column", mlog.Err(err))
return
}
})
return s.maxMessageSizeCached
}
func (s *SqlScheduledPostStore) GetPendingScheduledPosts(beforeTime, afterTime int64, lastScheduledPostId string, perPage uint64) ([]*model.ScheduledPost, error) {
query := s.getQueryBuilder().
Select(s.columns("")...).
From("ScheduledPosts").
Where(sq.Eq{"ErrorCode": ""}).
OrderBy("ScheduledAt DESC", "Id").
Limit(perPage)
if lastScheduledPostId == "" {
query = query.Where(sq.And{
sq.LtOrEq{"ScheduledAt": beforeTime},
sq.GtOrEq{"ScheduledAt": afterTime},
})
}
if lastScheduledPostId != "" {
query = query.
Where(sq.Or{
sq.And{
sq.LtOrEq{"ScheduledAt": beforeTime},
sq.GtOrEq{"ScheduledAt": afterTime},
},
sq.And{
sq.Eq{"ScheduledAt": beforeTime},
sq.Gt{"Id": lastScheduledPostId},
},
})
}
var scheduledPosts []*model.ScheduledPost
if err := s.GetReplicaX().SelectBuilder(&scheduledPosts, query); err != nil {
mlog.Error(
"SqlScheduledPostStore.GetPendingScheduledPosts: failed to fetch pending scheduled posts for processing",
mlog.Int("before_time", beforeTime),
mlog.String("last_scheduled_post_id", lastScheduledPostId),
mlog.Uint("items_per_page", perPage), mlog.Err(err),
)
return nil, errors.Wrapf(
err,
"SqlScheduledPostStore.GetPendingScheduledPosts: failed to fetch pending scheduled posts for processing, before_time: %d, last_scheduled_post_id: %s, items_per_page: %d",
beforeTime, lastScheduledPostId, perPage,
)
}
return scheduledPosts, nil
}
func (s *SqlScheduledPostStore) PermanentlyDeleteScheduledPosts(scheduledPostIDs []string) error {
if len(scheduledPostIDs) == 0 {
return nil
}
query := s.getQueryBuilder().
Delete("ScheduledPosts").
Where(sq.Eq{"Id": scheduledPostIDs})
sql, params, err := query.ToSql()
if err != nil {
errToReturn := errors.Wrapf(err, "PermanentlyDeleteScheduledPosts: failed to generate SQL query for permanently deleting batch of scheduled posts")
s.Logger().Error(errToReturn.Error())
return errToReturn
}
if _, err := s.GetMasterX().Exec(sql, params...); err != nil {
errToReturn := errors.Wrapf(err, "PermanentlyDeleteScheduledPosts: failed to delete batch of scheduled posts from database")
s.Logger().Error(errToReturn.Error())
return errToReturn
}
return nil
}
func (s *SqlScheduledPostStore) UpdatedScheduledPost(scheduledPost *model.ScheduledPost) error {
scheduledPost.PreUpdate()
builder := s.getQueryBuilder().
Update("ScheduledPosts").
SetMap(s.toUpdateMap(scheduledPost)).
Where(sq.Eq{"Id": scheduledPost.Id})
query, args, err := builder.ToSql()
if err != nil {
mlog.Error("SqlScheduledPostStore.UpdatedScheduledPost failed to generate SQL from updating scheduled posts", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.Err(err))
return errors.Wrap(err, "SqlScheduledPostStore.UpdatedScheduledPost failed to generate SQL from bulk updating scheduled posts")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
mlog.Error("SqlScheduledPostStore.UpdatedScheduledPost failed to update scheduled post", mlog.String("scheduled_post_id", scheduledPost.Id), mlog.Err(err))
return errors.Wrap(err, "SqlScheduledPostStore.UpdatedScheduledPost failed to update scheduled post")
}
return nil
}
func (s *SqlScheduledPostStore) toUpdateMap(scheduledPost *model.ScheduledPost) map[string]interface{} {
now := model.GetMillis()
return map[string]interface{}{
"UpdateAt": now,
"Message": scheduledPost.Message,
"Props": model.StringInterfaceToJSON(scheduledPost.GetProps()),
"FileIds": model.ArrayToJSON(scheduledPost.FileIds),
"Priority": model.StringInterfaceToJSON(scheduledPost.Priority),
"ScheduledAt": scheduledPost.ScheduledAt,
"ProcessedAt": now,
"ErrorCode": scheduledPost.ErrorCode,
}
}
func (s *SqlScheduledPostStore) Get(scheduledPostId string) (*model.ScheduledPost, error) {
query := s.getQueryBuilder().
Select(s.columns("")...).
From("ScheduledPosts").
Where(sq.Eq{
"Id": scheduledPostId,
})
scheduledPost := &model.ScheduledPost{}
if err := s.GetReplicaX().GetBuilder(scheduledPost, query); err != nil {
mlog.Error("SqlScheduledPostStore.Get: failed to get single scheduled post by ID from database", mlog.String("scheduled_post_id", scheduledPostId), mlog.Err(err))
return nil, errors.Wrapf(err, "SqlScheduledPostStore.Get: failed to get single scheduled post by ID from database, scheduledPostId: %s", scheduledPostId)
}
return scheduledPost, nil
}
func (s *SqlScheduledPostStore) UpdateOldScheduledPosts(beforeTime int64) error {
builder := s.getQueryBuilder().
Update("ScheduledPosts").
Set("ErrorCode", model.ScheduledPostErrorUnableToSend).
Set("ProcessedAt", model.GetMillis()).
Where(sq.And{
sq.Eq{"ErrorCode": ""},
sq.Lt{"ScheduledAt": beforeTime},
})
query, args, err := builder.ToSql()
if err != nil {
mlog.Error("SqlScheduledPostStore.UpdateOldScheduledPosts failed to generate SQL from updating old scheduled posts", mlog.Err(err))
return errors.Wrap(err, "SqlScheduledPostStore.UpdateOldScheduledPosts failed to generate SQL from updating old scheduled posts")
}
_, err = s.GetMasterX().Exec(query, args...)
if err != nil {
mlog.Error("SqlScheduledPostStore.UpdateOldScheduledPosts failed to update old scheduled posts", mlog.Err(err))
return errors.Wrap(err, "SqlScheduledPostStore.UpdateOldScheduledPosts failed to update old scheduled posts")
}
return nil
}

Просмотреть файл

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
)
func TestScheduledPostStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestScheduledPostStore)
}

Просмотреть файл

@@ -111,6 +111,7 @@ type SqlStoreStores struct {
postPersistentNotification store.PostPersistentNotificationStore
desktopTokens store.DesktopTokensStore
channelBookmarks store.ChannelBookmarkStore
scheduledPost store.ScheduledPostStore
}
type SqlStore struct {
@@ -236,6 +237,7 @@ func New(settings model.SqlSettings, logger mlog.LoggerIFace, metrics einterface
store.stores.postPersistentNotification = newSqlPostPersistentNotificationStore(store)
store.stores.desktopTokens = newSqlDesktopTokensStore(store, metrics)
store.stores.channelBookmarks = newSqlChannelBookmarkStore(store)
store.stores.scheduledPost = newScheduledPostStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@@ -1294,3 +1296,51 @@ func (ss *SqlStore) GetAppliedMigrations() ([]model.AppliedMigration, error) {
return migrations, nil
}
func (ss *SqlStore) determineMaxColumnSize(tableName, columnName string) (int, error) {
var columnSizeBytes int32
ss.getQueryPlaceholder()
if ss.DriverName() == model.DatabaseDriverPostgres {
if err := ss.GetReplicaX().Get(&columnSizeBytes, `
SELECT
COALESCE(character_maximum_length, 0)
FROM
information_schema.columns
WHERE
lower(table_name) = lower($1)
AND lower(column_name) = lower($2)
`, tableName, columnName); err != nil {
mlog.Warn("Unable to determine the maximum supported column size for Postgres", mlog.Err(err))
return 0, err
}
} else if ss.DriverName() == model.DatabaseDriverMysql {
if err := ss.GetReplicaX().Get(&columnSizeBytes, `
SELECT
COALESCE(CHARACTER_MAXIMUM_LENGTH, 0)
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
table_schema = DATABASE()
AND lower(table_name) = lower(?)
AND lower(column_name) = lower(?)
LIMIT 0, 1
`, tableName, columnName); err != nil {
mlog.Warn("Unable to determine the maximum supported column size for MySQL", mlog.Err(err))
return 0, err
}
} else {
mlog.Warn("No implementation found to determine the maximum supported column size")
}
// Assume a worst-case representation of four bytes per rune.
maxColumnSize := int(columnSizeBytes) / 4
mlog.Info("Column has size restrictions", mlog.String("table_name", tableName), mlog.String("column_name", columnName), mlog.Int("max_characters", maxColumnSize), mlog.Int("max_bytes", columnSizeBytes))
return maxColumnSize, nil
}
func (ss *SqlStore) ScheduledPost() store.ScheduledPostStore {
return ss.stores.scheduledPost
}

Просмотреть файл

@@ -91,6 +91,7 @@ type Store interface {
PostPersistentNotification() PostPersistentNotificationStore
DesktopTokens() DesktopTokensStore
ChannelBookmark() ChannelBookmarkStore
ScheduledPost() ScheduledPostStore
}
type RetentionPolicyStore interface {
@@ -1055,6 +1056,17 @@ type ChannelBookmarkStore interface {
GetBookmarksForChannelSince(channelID string, since int64) ([]*model.ChannelBookmarkWithFileInfo, error)
}
type ScheduledPostStore interface {
GetMaxMessageSize() int
CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error)
GetScheduledPostsForUser(userId, teamId string) ([]*model.ScheduledPost, error)
GetPendingScheduledPosts(beforeTime, afterTime int64, lastScheduledPostId string, perPage uint64) ([]*model.ScheduledPost, error)
PermanentlyDeleteScheduledPosts(scheduledPostIDs []string) error
UpdatedScheduledPost(scheduledPost *model.ScheduledPost) error
Get(scheduledPostId string) (*model.ScheduledPost, error)
UpdateOldScheduledPosts(beforeTime int64) error
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.

Просмотреть файл

@@ -0,0 +1,221 @@
// Code generated by mockery v2.42.2. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost/server/public/model"
mock "github.com/stretchr/testify/mock"
)
// ScheduledPostStore is an autogenerated mock type for the ScheduledPostStore type
type ScheduledPostStore struct {
mock.Mock
}
// CreateScheduledPost provides a mock function with given fields: scheduledPost
func (_m *ScheduledPostStore) CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
ret := _m.Called(scheduledPost)
if len(ret) == 0 {
panic("no return value specified for CreateScheduledPost")
}
var r0 *model.ScheduledPost
var r1 error
if rf, ok := ret.Get(0).(func(*model.ScheduledPost) (*model.ScheduledPost, error)); ok {
return rf(scheduledPost)
}
if rf, ok := ret.Get(0).(func(*model.ScheduledPost) *model.ScheduledPost); ok {
r0 = rf(scheduledPost)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ScheduledPost)
}
}
if rf, ok := ret.Get(1).(func(*model.ScheduledPost) error); ok {
r1 = rf(scheduledPost)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: scheduledPostId
func (_m *ScheduledPostStore) Get(scheduledPostId string) (*model.ScheduledPost, error) {
ret := _m.Called(scheduledPostId)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *model.ScheduledPost
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.ScheduledPost, error)); ok {
return rf(scheduledPostId)
}
if rf, ok := ret.Get(0).(func(string) *model.ScheduledPost); ok {
r0 = rf(scheduledPostId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ScheduledPost)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(scheduledPostId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMaxMessageSize provides a mock function with given fields:
func (_m *ScheduledPostStore) GetMaxMessageSize() int {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetMaxMessageSize")
}
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// GetPendingScheduledPosts provides a mock function with given fields: beforeTime, afterTime, lastScheduledPostId, perPage
func (_m *ScheduledPostStore) GetPendingScheduledPosts(beforeTime int64, afterTime int64, lastScheduledPostId string, perPage uint64) ([]*model.ScheduledPost, error) {
ret := _m.Called(beforeTime, afterTime, lastScheduledPostId, perPage)
if len(ret) == 0 {
panic("no return value specified for GetPendingScheduledPosts")
}
var r0 []*model.ScheduledPost
var r1 error
if rf, ok := ret.Get(0).(func(int64, int64, string, uint64) ([]*model.ScheduledPost, error)); ok {
return rf(beforeTime, afterTime, lastScheduledPostId, perPage)
}
if rf, ok := ret.Get(0).(func(int64, int64, string, uint64) []*model.ScheduledPost); ok {
r0 = rf(beforeTime, afterTime, lastScheduledPostId, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ScheduledPost)
}
}
if rf, ok := ret.Get(1).(func(int64, int64, string, uint64) error); ok {
r1 = rf(beforeTime, afterTime, lastScheduledPostId, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetScheduledPostsForUser provides a mock function with given fields: userId, teamId
func (_m *ScheduledPostStore) GetScheduledPostsForUser(userId string, teamId string) ([]*model.ScheduledPost, error) {
ret := _m.Called(userId, teamId)
if len(ret) == 0 {
panic("no return value specified for GetScheduledPostsForUser")
}
var r0 []*model.ScheduledPost
var r1 error
if rf, ok := ret.Get(0).(func(string, string) ([]*model.ScheduledPost, error)); ok {
return rf(userId, teamId)
}
if rf, ok := ret.Get(0).(func(string, string) []*model.ScheduledPost); ok {
r0 = rf(userId, teamId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.ScheduledPost)
}
}
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userId, teamId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PermanentlyDeleteScheduledPosts provides a mock function with given fields: scheduledPostIDs
func (_m *ScheduledPostStore) PermanentlyDeleteScheduledPosts(scheduledPostIDs []string) error {
ret := _m.Called(scheduledPostIDs)
if len(ret) == 0 {
panic("no return value specified for PermanentlyDeleteScheduledPosts")
}
var r0 error
if rf, ok := ret.Get(0).(func([]string) error); ok {
r0 = rf(scheduledPostIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateOldScheduledPosts provides a mock function with given fields: beforeTime
func (_m *ScheduledPostStore) UpdateOldScheduledPosts(beforeTime int64) error {
ret := _m.Called(beforeTime)
if len(ret) == 0 {
panic("no return value specified for UpdateOldScheduledPosts")
}
var r0 error
if rf, ok := ret.Get(0).(func(int64) error); ok {
r0 = rf(beforeTime)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdatedScheduledPost provides a mock function with given fields: scheduledPost
func (_m *ScheduledPostStore) UpdatedScheduledPost(scheduledPost *model.ScheduledPost) error {
ret := _m.Called(scheduledPost)
if len(ret) == 0 {
panic("no return value specified for UpdatedScheduledPost")
}
var r0 error
if rf, ok := ret.Get(0).(func(*model.ScheduledPost) error); ok {
r0 = rf(scheduledPost)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewScheduledPostStore creates a new instance of ScheduledPostStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewScheduledPostStore(t interface {
mock.TestingT
Cleanup(func())
}) *ScheduledPostStore {
mock := &ScheduledPostStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

Просмотреть файл

@@ -919,6 +919,26 @@ func (_m *Store) Role() store.RoleStore {
return r0
}
// ScheduledPost provides a mock function with given fields:
func (_m *Store) ScheduledPost() store.ScheduledPostStore {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for ScheduledPost")
}
var r0 store.ScheduledPostStore
if rf, ok := ret.Get(0).(func() store.ScheduledPostStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.ScheduledPostStore)
}
}
return r0
}
// Scheme provides a mock function with given fields:
func (_m *Store) Scheme() store.SchemeStore {
ret := _m.Called()

Просмотреть файл

@@ -0,0 +1,472 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestScheduledPostStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("CreateScheduledPost", func(t *testing.T) { testCreateScheduledPost(t, rctx, ss, s) })
t.Run("GetPendingScheduledPosts", func(t *testing.T) { testGetScheduledPosts(t, rctx, ss, s) })
t.Run("PermanentlyDeleteScheduledPosts", func(t *testing.T) { testPermanentlyDeleteScheduledPosts(t, rctx, ss, s) })
t.Run("UpdatedScheduledPost", func(t *testing.T) { testUpdatedScheduledPost(t, rctx, ss, s) })
t.Run("UpdateOldScheduledPosts", func(t *testing.T) { testUpdateOldScheduledPosts(t, rctx, ss, s) })
}
func testCreateScheduledPost(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
channel := &model.Channel{
TeamId: "team_id_1",
Type: model.ChannelTypeOpen,
Name: "channel_name",
DisplayName: "Channel Name",
}
createdChannel, err := ss.Channel().Save(rctx, channel, 1000)
assert.NoError(t, err)
defer func() {
_ = ss.Channel().PermanentDelete(rctx, createdChannel.Id)
}()
t.Run("base case", func(t *testing.T) {
userId := model.NewId()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000, // 100 seconds in the future
}
createdScheduledPost, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost.Id)
defer func() {
_ = ss.ScheduledPost().PermanentlyDeleteScheduledPosts([]string{createdScheduledPost.Id})
}()
scheduledPostsFromDatabase, err := ss.ScheduledPost().GetScheduledPostsForUser(userId, "team_id_1")
assert.NoError(t, err)
require.Equal(t, 1, len(scheduledPostsFromDatabase))
assert.Equal(t, scheduledPost.Id, scheduledPostsFromDatabase[0].Id)
})
t.Run("scheduling in past SHOULD BE allowed", func(t *testing.T) {
// this is only allowed in store layer and user won't be able to do so as the checks
// in app layer would stop them.
userId := model.NewId()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() - 100000, // 100 seconds in the past
}
_, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
defer func() {
_ = ss.ScheduledPost().PermanentlyDeleteScheduledPosts([]string{scheduledPost.Id})
}()
})
}
func testGetScheduledPosts(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("should handle no scheduled posts exist", func(t *testing.T) {
apr2022 := time.Date(2100, time.April, 1, 1, 0, 0, 0, time.UTC)
afterTime := time.Date(2100, time.March, 1, 1, 0, 0, 0, time.UTC)
scheduledPosts, err := ss.ScheduledPost().GetPendingScheduledPosts(model.GetMillisForTime(apr2022), model.GetMillisForTime(afterTime), "", 10)
assert.NoError(t, err)
assert.Equal(t, 0, len(scheduledPosts))
})
t.Run("base case", func(t *testing.T) {
// creating some sample scheduled posts
// Create a time object for 1 January 2100, 1 AM
jan2100 := time.Date(2100, time.January, 1, 1, 0, 0, 0, time.UTC)
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillisForTime(jan2100),
}
createdScheduledPost1, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost1)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost1.Id)
feb2100 := time.Date(2100, time.February, 1, 1, 0, 0, 0, time.UTC)
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillisForTime(feb2100),
}
createdScheduledPost2, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost2)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost2.Id)
mar2100 := time.Date(2100, time.March, 1, 1, 0, 0, 0, time.UTC)
scheduledPost3 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillisForTime(mar2100),
}
createdScheduledPost3, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost3)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost3.Id)
defer func() {
_ = ss.ScheduledPost().PermanentlyDeleteScheduledPosts([]string{
createdScheduledPost1.Id,
createdScheduledPost2.Id,
createdScheduledPost3.Id,
})
}()
apr2022 := time.Date(2100, time.April, 1, 1, 0, 0, 0, time.UTC)
afterTime := time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC)
scheduledPosts, err := ss.ScheduledPost().GetPendingScheduledPosts(model.GetMillisForTime(apr2022), model.GetMillisForTime(afterTime), "", 10)
assert.NoError(t, err)
assert.Equal(t, 3, len(scheduledPosts))
mar2100midnight := time.Date(2100, time.March, 1, 0, 0, 0, 0, time.UTC)
afterTime = time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC)
scheduledPosts, err = ss.ScheduledPost().GetPendingScheduledPosts(model.GetMillisForTime(mar2100midnight), model.GetMillisForTime(afterTime), "", 10)
assert.NoError(t, err)
assert.Equal(t, 2, len(scheduledPosts))
jan2100Midnight := time.Date(2100, time.January, 1, 0, 0, 0, 0, time.UTC)
afterTime = time.Date(2099, time.December, 31, 0, 0, 0, 0, time.UTC)
scheduledPosts, err = ss.ScheduledPost().GetPendingScheduledPosts(model.GetMillisForTime(jan2100Midnight), model.GetMillisForTime(afterTime), "", 10)
assert.NoError(t, err)
assert.Equal(t, 0, len(scheduledPosts))
})
}
func testPermanentlyDeleteScheduledPosts(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
scheduledPostIDs := []string{}
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() + 100000,
}
createdScheduledPost, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost.Id)
scheduledPostIDs = append(scheduledPostIDs, createdScheduledPost.Id)
scheduledPost = &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: "this is a scheduled post 2",
},
ScheduledAt: model.GetMillis() + 100000,
}
createdScheduledPost, err = ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost.Id)
scheduledPostIDs = append(scheduledPostIDs, createdScheduledPost.Id)
scheduledPost = &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: "this is a scheduled post 3",
},
ScheduledAt: model.GetMillis() + 100000,
}
createdScheduledPost, err = ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost.Id)
scheduledPostIDs = append(scheduledPostIDs, createdScheduledPost.Id)
scheduledPost = &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: model.NewId(),
ChannelId: model.NewId(),
Message: "this is a scheduled post 4",
},
ScheduledAt: model.GetMillis() + 100000,
}
createdScheduledPost, err = ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost.Id)
scheduledPostIDs = append(scheduledPostIDs, createdScheduledPost.Id)
// verify 4 scheduled posts exist
scheduledPosts, err := ss.ScheduledPost().GetPendingScheduledPosts(model.GetMillis()+50000000, model.GetMillis()-100000000, "", 10)
assert.NoError(t, err)
assert.Equal(t, 4, len(scheduledPosts))
// now we'll delete all scheduled posts
err = ss.ScheduledPost().PermanentlyDeleteScheduledPosts(scheduledPostIDs)
assert.NoError(t, err)
// now there should be no posts
scheduledPosts, err = ss.ScheduledPost().GetPendingScheduledPosts(model.GetMillis()+50000000, model.GetMillis()-100000000, "", 10)
assert.NoError(t, err)
assert.Equal(t, 0, len(scheduledPosts))
}
func testUpdatedScheduledPost(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
channel := &model.Channel{
TeamId: "team_id_1",
Type: model.ChannelTypeOpen,
Name: "channel_name",
DisplayName: "Channel Name",
}
createdChannel, err := ss.Channel().Save(rctx, channel, 1000)
assert.NoError(t, err)
defer func() {
_ = ss.Channel().PermanentDelete(rctx, createdChannel.Id)
}()
t.Run("it should update only limited fields", func(t *testing.T) {
userId := model.NewId()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis(),
}
createdScheduledPost, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost.Id)
// now we'll update the scheduled post
updateTimestamp := model.GetMillis()
fileID1 := model.NewId()
fileID2 := model.NewId()
newScheduledAt := model.GetMillis()
newUserId := model.NewId()
updateSchedulePost := &model.ScheduledPost{
Id: createdScheduledPost.Id,
ScheduledAt: newScheduledAt,
ErrorCode: "test_error_code",
Draft: model.Draft{
CreateAt: model.GetMillis(),
Message: "updated message",
UpdateAt: updateTimestamp,
UserId: newUserId, // this should not update
ChannelId: model.NewId(), // this should not update
FileIds: []string{fileID1, fileID2},
Priority: model.StringInterface{
"priority": "urgent",
"requested_ack": false,
"persistent_notifications": false,
},
},
}
err = ss.ScheduledPost().UpdatedScheduledPost(updateSchedulePost)
assert.NoError(t, err)
// now we'll get it and verify that intended fields updated and other fields did not
userScheduledPosts, err := ss.ScheduledPost().GetScheduledPostsForUser(userId, channel.TeamId)
assert.NoError(t, err)
assert.Equal(t, 1, len(userScheduledPosts))
// fields that should have changed
assert.Equal(t, newScheduledAt, userScheduledPosts[0].ScheduledAt)
assert.Equal(t, "test_error_code", userScheduledPosts[0].ErrorCode)
assert.Equal(t, "updated message", userScheduledPosts[0].Message)
assert.Equal(t, 2, len(userScheduledPosts[0].FileIds))
assert.Equal(t, "urgent", userScheduledPosts[0].Priority["priority"])
assert.Equal(t, false, userScheduledPosts[0].Priority["requested_ack"])
assert.Equal(t, false, userScheduledPosts[0].Priority["persistent_notifications"])
// fields that should not have changed. Checking them against the original value
assert.Equal(t, createdScheduledPost.Id, userScheduledPosts[0].Id)
assert.Equal(t, userId, userScheduledPosts[0].UserId)
assert.Equal(t, channel.Id, userScheduledPosts[0].ChannelId)
})
t.Run("it should update old scheduled post", func(t *testing.T) {
userId := model.NewId()
scheduledPost := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: model.GetMillis() - (24 * 60 * 60 * 1000), // 1 day in the past
}
createdScheduledPost, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost.Id)
// now we'll update the scheduled post
processedAt := model.GetMillis()
scheduledPost.ProcessedAt = processedAt
scheduledPost.ErrorCode = model.ScheduledPostErrorUnknownError
err = ss.ScheduledPost().UpdatedScheduledPost(scheduledPost)
assert.NoError(t, err)
updatedScheduledPost, err := ss.ScheduledPost().Get(scheduledPost.Id)
assert.NoError(t, err)
assert.Equal(t, processedAt, updatedScheduledPost.ProcessedAt)
assert.Equal(t, model.ScheduledPostErrorUnknownError, updatedScheduledPost.ErrorCode)
})
}
func testUpdateOldScheduledPosts(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
setupScheduledPosts := func(baseTime int64, userId, teamId string) func() {
channel := &model.Channel{
TeamId: teamId,
Type: model.ChannelTypeOpen,
Name: "channel_name",
DisplayName: "Channel Name",
}
createdChannel, err := ss.Channel().Save(rctx, channel, 1000)
assert.NoError(t, err)
// Scheduled post 1
scheduledPost1 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is a scheduled post",
},
ScheduledAt: baseTime + 86400000, // 1 day in the future
}
createdScheduledPost1, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost1)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost1.Id)
time.Sleep(100 * time.Millisecond)
// Scheduled post 2
scheduledPost2 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is second scheduled post",
},
ScheduledAt: baseTime + (2 * 86400000), // 2 days in the future
}
createdScheduledPost2, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost2)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost2.Id)
time.Sleep(100 * time.Millisecond)
// Scheduled post 3
scheduledPost3 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is third scheduled post",
},
ScheduledAt: baseTime + (3 * 86400000), // 3 days in the future
}
createdScheduledPost3, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost3)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost3.Id)
time.Sleep(100 * time.Millisecond)
// Scheduled post 4
scheduledPost4 := &model.ScheduledPost{
Draft: model.Draft{
CreateAt: model.GetMillis(),
UserId: userId,
ChannelId: createdChannel.Id,
Message: "this is fourth scheduled post",
},
ScheduledAt: baseTime + (4 * 86400000), // 4 days in the future
}
createdScheduledPost4, err := ss.ScheduledPost().CreateScheduledPost(scheduledPost4)
assert.NoError(t, err)
assert.NotEmpty(t, createdScheduledPost4.Id)
return func() {
_ = ss.ScheduledPost().PermanentlyDeleteScheduledPosts([]string{
createdScheduledPost1.Id,
createdScheduledPost2.Id,
createdScheduledPost3.Id,
createdScheduledPost4.Id,
})
_ = ss.Channel().PermanentDelete(rctx, createdChannel.Id)
}
}
t.Run("should update only old scheduled posts", func(t *testing.T) {
now := model.GetMillis()
userId := model.NewId()
teamId := model.NewId()
cleanup := setupScheduledPosts(now, userId, teamId)
defer cleanup()
err := ss.ScheduledPost().UpdateOldScheduledPosts(now + 2.5*86400000) // marking all posts older than 2 days from now
assert.NoError(t, err)
scheduledPosts, err := ss.ScheduledPost().GetScheduledPostsForUser(userId, teamId)
assert.NoError(t, err)
assert.Equal(t, 4, len(scheduledPosts))
assert.Equal(t, model.ScheduledPostErrorUnableToSend, scheduledPosts[0].ErrorCode)
assert.Equal(t, model.ScheduledPostErrorUnableToSend, scheduledPosts[1].ErrorCode)
assert.Equal(t, "", scheduledPosts[2].ErrorCode)
assert.Equal(t, "", scheduledPosts[3].ErrorCode)
})
}

Просмотреть файл

@@ -65,6 +65,7 @@ type Store struct {
PostPersistentNotificationStore mocks.PostPersistentNotificationStore
DesktopTokensStore mocks.DesktopTokensStore
ChannelBookmarkStore mocks.ChannelBookmarkStore
ScheduledPostStore mocks.ScheduledPostStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
@@ -117,6 +118,7 @@ func (s *Store) Group() store.GroupStore { return &s.GroupSt
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore }
func (s *Store) ScheduledPost() store.ScheduledPostStore { return &s.ScheduledPostStore }
func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return &s.PostAcknowledgementStore
}
@@ -188,5 +190,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
&s.PostPersistentNotificationStore,
&s.DesktopTokensStore,
&s.ChannelBookmarkStore,
&s.ScheduledPostStore,
)
}

Просмотреть файл

@@ -50,6 +50,7 @@ type TimerLayer struct {
RemoteClusterStore store.RemoteClusterStore
RetentionPolicyStore store.RetentionPolicyStore
RoleStore store.RoleStore
ScheduledPostStore store.ScheduledPostStore
SchemeStore store.SchemeStore
SessionStore store.SessionStore
SharedChannelStore store.SharedChannelStore
@@ -190,6 +191,10 @@ func (s *TimerLayer) Role() store.RoleStore {
return s.RoleStore
}
func (s *TimerLayer) ScheduledPost() store.ScheduledPostStore {
return s.ScheduledPostStore
}
func (s *TimerLayer) Scheme() store.SchemeStore {
return s.SchemeStore
}
@@ -401,6 +406,11 @@ type TimerLayerRoleStore struct {
Root *TimerLayer
}
type TimerLayerScheduledPostStore struct {
store.ScheduledPostStore
Root *TimerLayer
}
type TimerLayerSchemeStore struct {
store.SchemeStore
Root *TimerLayer
@@ -7753,6 +7763,134 @@ func (s *TimerLayerRoleStore) Save(role *model.Role) (*model.Role, error) {
return result, err
}
func (s *TimerLayerScheduledPostStore) CreateScheduledPost(scheduledPost *model.ScheduledPost) (*model.ScheduledPost, error) {
start := time.Now()
result, err := s.ScheduledPostStore.CreateScheduledPost(scheduledPost)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.CreateScheduledPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerScheduledPostStore) Get(scheduledPostId string) (*model.ScheduledPost, error) {
start := time.Now()
result, err := s.ScheduledPostStore.Get(scheduledPostId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerScheduledPostStore) GetMaxMessageSize() int {
start := time.Now()
result := s.ScheduledPostStore.GetMaxMessageSize()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if true {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.GetMaxMessageSize", success, elapsed)
}
return result
}
func (s *TimerLayerScheduledPostStore) GetPendingScheduledPosts(beforeTime int64, afterTime int64, lastScheduledPostId string, perPage uint64) ([]*model.ScheduledPost, error) {
start := time.Now()
result, err := s.ScheduledPostStore.GetPendingScheduledPosts(beforeTime, afterTime, lastScheduledPostId, perPage)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.GetPendingScheduledPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerScheduledPostStore) GetScheduledPostsForUser(userId string, teamId string) ([]*model.ScheduledPost, error) {
start := time.Now()
result, err := s.ScheduledPostStore.GetScheduledPostsForUser(userId, teamId)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.GetScheduledPostsForUser", success, elapsed)
}
return result, err
}
func (s *TimerLayerScheduledPostStore) PermanentlyDeleteScheduledPosts(scheduledPostIDs []string) error {
start := time.Now()
err := s.ScheduledPostStore.PermanentlyDeleteScheduledPosts(scheduledPostIDs)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.PermanentlyDeleteScheduledPosts", success, elapsed)
}
return err
}
func (s *TimerLayerScheduledPostStore) UpdateOldScheduledPosts(beforeTime int64) error {
start := time.Now()
err := s.ScheduledPostStore.UpdateOldScheduledPosts(beforeTime)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.UpdateOldScheduledPosts", success, elapsed)
}
return err
}
func (s *TimerLayerScheduledPostStore) UpdatedScheduledPost(scheduledPost *model.ScheduledPost) error {
start := time.Now()
err := s.ScheduledPostStore.UpdatedScheduledPost(scheduledPost)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ScheduledPostStore.UpdatedScheduledPost", success, elapsed)
}
return err
}
func (s *TimerLayerSchemeStore) CountByScope(scope string) (int64, error) {
start := time.Now()
@@ -12313,6 +12451,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.RemoteClusterStore = &TimerLayerRemoteClusterStore{RemoteClusterStore: childStore.RemoteCluster(), Root: &newStore}
newStore.RetentionPolicyStore = &TimerLayerRetentionPolicyStore{RetentionPolicyStore: childStore.RetentionPolicy(), Root: &newStore}
newStore.RoleStore = &TimerLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
newStore.ScheduledPostStore = &TimerLayerScheduledPostStore{ScheduledPostStore: childStore.ScheduledPost(), Root: &newStore}
newStore.SchemeStore = &TimerLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
newStore.SessionStore = &TimerLayerSessionStore{SessionStore: childStore.Session(), Root: &newStore}
newStore.SharedChannelStore = &TimerLayerSharedChannelStore{SharedChannelStore: childStore.SharedChannel(), Root: &newStore}

Просмотреть файл

@@ -222,10 +222,8 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
if license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise {
props["EnableCustomGroups"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomGroups)
}
if license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise {
props["PostAcknowledgements"] = "true"
props["ScheduledPosts"] = strconv.FormatBool(*c.ServiceSettings.ScheduledPosts)
}
}

Просмотреть файл

@@ -2838,6 +2838,14 @@
"id": "api.saml.invalid_email_token.app_error",
"translation": "Invalid email_token"
},
{
"id": "api.scheduled_posts.feature_disabled",
"translation": "scheduled posts feature is disabled"
},
{
"id": "api.scheduled_posts.license_error",
"translation": "Scheduled posts feature requires a license"
},
{
"id": "api.scheme.create_scheme.license.error",
"translation": "Your license does not support creating permissions schemes."
@@ -4958,6 +4966,22 @@
"id": "app.custom_group.unique_name",
"translation": "group name is not unique"
},
{
"id": "app.delete_scheduled_post.delete_error",
"translation": "Failed to delete scheduled post from database."
},
{
"id": "app.delete_scheduled_post.delete_permission.error",
"translation": "You do not have permission to delete this resource."
},
{
"id": "app.delete_scheduled_post.existing_scheduled_post.not_exist",
"translation": "Scheduled post does not exist."
},
{
"id": "app.delete_scheduled_post.get_scheduled_post.error",
"translation": "Unable to fetch existing scheduled post from database."
},
{
"id": "app.desktop_token.generateServerToken.invalid_or_expired",
"translation": "Token does not exist or is expired"
@@ -5138,6 +5162,10 @@
"id": "app.file_info.set_searchable_content.app_error",
"translation": "Unable to set the searchable content of the file."
},
{
"id": "app.get_user_team_scheduled_posts.error",
"translation": "Error occurred fetching scheduled posts."
},
{
"id": "app.group.crud_permission",
"translation": "Unable to perform operation for that source type."
@@ -6482,6 +6510,14 @@
"id": "app.save_report_chunk.unsupported_format",
"translation": "Unsupported report format."
},
{
"id": "app.save_scheduled_post.channel_deleted.app_error",
"translation": "Cannot schedule post in an archived channel."
},
{
"id": "app.save_scheduled_post.save.app_error",
"translation": "Error occurred saving the scheduled post."
},
{
"id": "app.scheme.delete.app_error",
"translation": "Unable to delete this scheme."
@@ -6806,6 +6842,22 @@
"id": "app.update_error",
"translation": "update error"
},
{
"id": "app.update_scheduled_post.existing_scheduled_post.not_exist",
"translation": "Scheduled post does not exist."
},
{
"id": "app.update_scheduled_post.get_scheduled_post.error",
"translation": "Unable to fetch existing scheduled post from database."
},
{
"id": "app.update_scheduled_post.update.error",
"translation": "Failed to save updated scheduled post in database."
},
{
"id": "app.update_scheduled_post.update_permission.error",
"translation": "You do not have permission to update this resource."
},
{
"id": "app.upload.create.cannot_upload_to_deleted_channel.app_error",
"translation": "Cannot upload to a deleted channel."
@@ -9662,6 +9714,22 @@
"id": "model.reporting_base_options.is_valid.bad_date_range",
"translation": "Date range provided is invalid."
},
{
"id": "model.scheduled_post.is_valid.empty_post.app_error",
"translation": "Cannot schedule an empty post. Scheduled post must have at least a message or file attachments."
},
{
"id": "model.scheduled_post.is_valid.id.app_error",
"translation": "Scheduled post must have an ID."
},
{
"id": "model.scheduled_post.is_valid.processed_at.app_error",
"translation": "Invalid processed at time."
},
{
"id": "model.scheduled_post.is_valid.scheduled_at.app_error",
"translation": "Invalid scheduled at time."
},
{
"id": "model.scheme.is_valid.app_error",
"translation": "Invalid scheme."

Просмотреть файл

@@ -612,6 +612,73 @@ func (c *Client4) GetServerLimits(ctx context.Context) (*ServerLimits, *Response
return &serverLimits, BuildResponse(r), nil
}
func (c *Client4) CreateScheduledPost(ctx context.Context, scheduledPost *ScheduledPost) (*ScheduledPost, *Response, error) {
buf, err := json.Marshal(scheduledPost)
if err != nil {
return nil, nil, NewAppError("CreateScheduledPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPost(ctx, c.postsRoute()+"/schedule", string(buf))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var createdScheduledPost ScheduledPost
if err := json.NewDecoder(r.Body).Decode(&createdScheduledPost); err != nil {
return nil, nil, NewAppError("CreateScheduledPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &createdScheduledPost, BuildResponse(r), nil
}
func (c *Client4) GetUserScheduledPosts(ctx context.Context, teamId string, includeDirectChannels bool) (map[string][]*ScheduledPost, *Response, error) {
query := url.Values{}
query.Set("includeDirectChannels", fmt.Sprintf("%t", includeDirectChannels))
r, err := c.DoAPIGet(ctx, c.postsRoute()+"/scheduled/team/"+teamId+"?"+query.Encode(), "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var scheduledPostsByTeam map[string][]*ScheduledPost
if err := json.NewDecoder(r.Body).Decode(&scheduledPostsByTeam); err != nil {
return nil, nil, NewAppError("GetUserScheduledPosts", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return scheduledPostsByTeam, BuildResponse(r), nil
}
func (c *Client4) UpdateScheduledPost(ctx context.Context, scheduledPost *ScheduledPost) (*ScheduledPost, *Response, error) {
buf, err := json.Marshal(scheduledPost)
if err != nil {
return nil, nil, NewAppError("UpdateScheduledPost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
r, err := c.DoAPIPut(ctx, c.postsRoute()+"/schedule/"+scheduledPost.Id, string(buf))
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var updatedScheduledPost ScheduledPost
if err := json.NewDecoder(r.Body).Decode(&updatedScheduledPost); err != nil {
return nil, nil, NewAppError("UpdateScheduledPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &updatedScheduledPost, BuildResponse(r), nil
}
func (c *Client4) DeleteScheduledPost(ctx context.Context, scheduledPostId string) (*ScheduledPost, *Response, error) {
r, err := c.DoAPIDelete(ctx, c.postsRoute()+"/schedule/"+scheduledPostId)
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var deletedScheduledPost ScheduledPost
if err := json.NewDecoder(r.Body).Decode(&deletedScheduledPost); err != nil {
return nil, nil, NewAppError("DeleteScheduledPost", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return &deletedScheduledPost, BuildResponse(r), nil
}
func (c *Client4) bookmarksRoute(channelId string) string {
return c.channelRoute(channelId) + "/bookmarks"
}

Просмотреть файл

@@ -419,6 +419,7 @@ type ServiceSettings struct {
RefreshPostStatsRunTime *string `access:"site_users_and_teams"`
MaximumPayloadSizeBytes *int64 `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
MaximumURLLength *int `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ScheduledPosts *bool `access:"site_posts"`
}
var MattermostGiphySdkKey string
@@ -942,6 +943,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
if s.MaximumURLLength == nil {
s.MaximumURLLength = NewPointer(ServiceSettingsDefaultMaxURLLength)
}
if s.ScheduledPosts == nil {
s.ScheduledPosts = NewPointer(true)
}
}
type CacheSettings struct {

Просмотреть файл

@@ -27,6 +27,14 @@ type Draft struct {
}
func (o *Draft) IsValid(maxDraftSize int) *AppError {
if utf8.RuneCountInString(o.Message) > maxDraftSize {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.msg.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
return o.BaseIsValid()
}
func (o *Draft) BaseIsValid() *AppError {
if o.CreateAt == 0 {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.create_at.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
@@ -47,10 +55,6 @@ func (o *Draft) IsValid(maxDraftSize int) *AppError {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.root_id.app_error", nil, "", http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Message) > maxDraftSize {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.msg.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}
if utf8.RuneCountInString(ArrayToJSON(o.FileIds)) > PostFileidsMaxRunes {
return NewAppError("Drafts.IsValid", "model.draft.is_valid.file_ids.app_error", nil, "channelid="+o.ChannelId, http.StatusBadRequest)
}

Просмотреть файл

@@ -458,3 +458,10 @@ func (lr *LicenseRecord) IsValid() *AppError {
func (lr *LicenseRecord) PreSave() {
lr.CreateAt = GetMillis()
}
func MinimumProfessionalProvidedLicense(license *License) *AppError {
if license == nil || (license.SkuShortName != LicenseShortSkuProfessional && license.SkuShortName != LicenseShortSkuEnterprise) {
return NewAppError("", NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
}
return nil
}

Просмотреть файл

@@ -2604,6 +2604,10 @@ func init() {
}
func MakePermissionError(s *Session, permissions []*Permission) *AppError {
return MakePermissionErrorForUser(s.UserId, permissions)
}
func MakePermissionErrorForUser(userId string, permissions []*Permission) *AppError {
permissionsStr := "permission="
for i, permission := range permissions {
permissionsStr += permission.Id
@@ -2611,5 +2615,5 @@ func MakePermissionError(s *Session, permissions []*Permission) *AppError {
permissionsStr += ","
}
}
return NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+s.UserId+", "+permissionsStr, http.StatusForbidden)
return NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+userId+", "+permissionsStr, http.StatusForbidden)
}

Просмотреть файл

@@ -523,17 +523,17 @@ func (o *Post) SanitizeInput() {
}
func (o *Post) ContainsIntegrationsReservedProps() []string {
return containsIntegrationsReservedProps(o.GetProps())
return ContainsIntegrationsReservedProps(o.GetProps())
}
func (o *PostPatch) ContainsIntegrationsReservedProps() []string {
if o == nil || o.Props == nil {
return nil
}
return containsIntegrationsReservedProps(*o.Props)
return ContainsIntegrationsReservedProps(*o.Props)
}
func containsIntegrationsReservedProps(props StringInterface) []string {
func ContainsIntegrationsReservedProps(props StringInterface) []string {
foundProps := []string{}
if props != nil {

171
server/public/model/scheduled_post.go Обычный файл
Просмотреть файл

@@ -0,0 +1,171 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"net/http"
)
const (
ScheduledPostErrorUnknownError = "unknown"
ScheduledPostErrorCodeChannelArchived = "channel_archived"
ScheduledPostErrorCodeChannelNotFound = "channel_not_found"
ScheduledPostErrorCodeUserDoesNotExist = "user_missing"
ScheduledPostErrorCodeUserDeleted = "user_deleted"
ScheduledPostErrorCodeNoChannelPermission = "no_channel_permission"
ScheduledPostErrorNoChannelMember = "no_channel_member"
ScheduledPostErrorThreadDeleted = "thread_deleted"
ScheduledPostErrorUnableToSend = "unable_to_send"
ScheduledPostErrorInvalidPost = "invalid_post"
)
// allow scheduled posts to be created up to
// this much time in the past. While this ir primarily added for reliable test cases,
// it also helps with flaky and slow network connection between the client and the server,
const scheduledPostMaxTimeGap = -5000
type ScheduledPost struct {
Draft
Id string `json:"id"`
ScheduledAt int64 `json:"scheduled_at"`
ProcessedAt int64 `json:"processed_at"`
ErrorCode string `json:"error_code"`
}
func (s *ScheduledPost) IsValid(maxMessageSize int) *AppError {
draftAppErr := s.Draft.IsValid(maxMessageSize)
if draftAppErr != nil {
return draftAppErr
}
return s.BaseIsValid()
}
func (s *ScheduledPost) BaseIsValid() *AppError {
if draftAppErr := s.Draft.BaseIsValid(); draftAppErr != nil {
return draftAppErr
}
if s.Id == "" {
return NewAppError("ScheduledPost.IsValid", "model.scheduled_post.is_valid.id.app_error", nil, "id="+s.Id, http.StatusBadRequest)
}
if len(s.Message) == 0 && len(s.FileIds) == 0 {
return NewAppError("ScheduledPost.IsValid", "model.scheduled_post.is_valid.empty_post.app_error", nil, "id="+s.Id, http.StatusBadRequest)
}
if (s.ScheduledAt - GetMillis()) < scheduledPostMaxTimeGap {
return NewAppError("ScheduledPost.IsValid", "model.scheduled_post.is_valid.scheduled_at.app_error", nil, "id="+s.Id, http.StatusBadRequest)
}
if s.ProcessedAt < 0 {
return NewAppError("ScheduledPost.IsValid", "model.scheduled_post.is_valid.processed_at.app_error", nil, "id="+s.Id, http.StatusBadRequest)
}
return nil
}
func (s *ScheduledPost) PreSave() {
if s.Id == "" {
s.Id = NewId()
}
s.ProcessedAt = 0
s.ErrorCode = ""
s.Draft.PreSave()
}
func (s *ScheduledPost) PreUpdate() {
s.Draft.UpdateAt = GetMillis()
s.Draft.PreCommit()
}
// ToPost converts a scheduled post toa regular, mattermost post object.
func (s *ScheduledPost) ToPost() (*Post, error) {
post := &Post{
UserId: s.UserId,
ChannelId: s.ChannelId,
Message: s.Message,
FileIds: s.FileIds,
RootId: s.RootId,
Metadata: s.Metadata,
}
for key, value := range s.GetProps() {
post.AddProp(key, value)
}
if len(s.Priority) > 0 {
priority, ok := s.Priority["priority"].(string)
if !ok {
return nil, fmt.Errorf(`ScheduledPost.ToPost: priority is not a string. ScheduledPost.Priority: %v`, s.Priority)
}
requestedAck, ok := s.Priority["requested_ack"].(bool)
if !ok {
return nil, fmt.Errorf(`ScheduledPost.ToPost: requested_ack is not a bool. ScheduledPost.Priority: %v`, s.Priority)
}
persistentNotifications, ok := s.Priority["persistent_notifications"].(bool)
if !ok {
return nil, fmt.Errorf(`ScheduledPost.ToPost: persistent_notifications is not a bool. ScheduledPost.Priority: %v`, s.Priority)
}
if post.Metadata == nil {
post.Metadata = &PostMetadata{}
}
post.Metadata.Priority = &PostPriority{
Priority: NewPointer(priority),
RequestedAck: NewPointer(requestedAck),
PersistentNotifications: NewPointer(persistentNotifications),
}
}
return post, nil
}
func (s *ScheduledPost) Auditable() map[string]interface{} {
var metaData map[string]any
if s.Metadata != nil {
metaData = s.Metadata.Auditable()
}
return map[string]interface{}{
"id": s.Id,
"create_at": s.CreateAt,
"update_at": s.UpdateAt,
"user_id": s.UserId,
"channel_id": s.ChannelId,
"root_id": s.RootId,
"props": s.GetProps(),
"file_ids": s.FileIds,
"metadata": metaData,
}
}
func (s *ScheduledPost) RestoreNonUpdatableFields(originalScheduledPost *ScheduledPost) {
s.Id = originalScheduledPost.Id
s.CreateAt = originalScheduledPost.CreateAt
s.UserId = originalScheduledPost.UserId
s.ChannelId = originalScheduledPost.ChannelId
s.RootId = originalScheduledPost.RootId
}
func (s *ScheduledPost) SanitizeInput() {
s.CreateAt = 0
if s.Metadata != nil {
s.Metadata.Embeds = nil
}
}
func (s *ScheduledPost) GetPriority() *PostPriority {
if s.Metadata == nil {
return nil
}
return s.Metadata.Priority
}