Merge branch 'master' into mpa-playbooks
Этот коммит содержится в:
@@ -69,8 +69,6 @@ type AppIface interface {
|
||||
// If includeRemovedMembers is true, then channel members who left or were removed from the channel will
|
||||
// be included; otherwise, they will be excluded.
|
||||
ChannelMembersToAdd(since int64, channelID *string, includeRemovedMembers bool) ([]*model.UserChannelIDPair, *model.AppError)
|
||||
// CheckFreemiumLimitsForConfigSave returns an error if the configuration being saved violates a cloud plan's limits
|
||||
CheckFreemiumLimitsForConfigSave(oldConfig, newConfig *model.Config) *model.AppError
|
||||
// CheckProviderAttributes returns the empty string if the patch can be applied without
|
||||
// overriding attributes set by the user's login provider; otherwise, the name of the offending
|
||||
// field is returned.
|
||||
@@ -85,6 +83,8 @@ type AppIface interface {
|
||||
ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.UserPatch, sysadmin bool) (*model.User, *model.AppError)
|
||||
// ConvertUserToBot converts a user to bot.
|
||||
ConvertUserToBot(user *model.User) (*model.Bot, *model.AppError)
|
||||
// Create/ Update a subscription history event
|
||||
SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error)
|
||||
// CreateBot creates the given bot and corresponding user.
|
||||
CreateBot(c request.CTX, bot *model.Bot) (*model.Bot, *model.AppError)
|
||||
// CreateChannelScheme creates a new Scheme of scope channel and assigns it to the channel.
|
||||
@@ -184,8 +184,6 @@ type AppIface interface {
|
||||
GetFilteredUsersStats(options *model.UserCountOptions) (*model.UsersStats, *model.AppError)
|
||||
// GetGroupsByTeam returns the paged list and the total count of group associated to the given team.
|
||||
GetGroupsByTeam(teamID string, opts model.GroupSearchOpts) ([]*model.GroupWithSchemeAdmin, int, *model.AppError)
|
||||
// GetIntegrationsUsage returns usage information on enabled integrations
|
||||
GetIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError)
|
||||
// GetKnownUsers returns the list of user ids of users with any direct
|
||||
// relationship with a user. That means any user sharing any channel, including
|
||||
// direct and group channels.
|
||||
@@ -478,6 +476,7 @@ type AppIface interface {
|
||||
CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)
|
||||
CreateCommand(cmd *model.Command) (*model.Command, *model.AppError)
|
||||
CreateCommandWebhook(commandID string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError)
|
||||
CreateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError)
|
||||
CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError)
|
||||
CreateGroup(group *model.Group) (*model.Group, *model.AppError)
|
||||
CreateGroupChannel(c request.CTX, userIDs []string, creatorId string) (*model.Channel, *model.AppError)
|
||||
@@ -513,11 +512,13 @@ type AppIface interface {
|
||||
DeactivateGuests(c *request.Context) *model.AppError
|
||||
DeactivateMfa(userID string) *model.AppError
|
||||
DeauthorizeOAuthAppForUser(userID, appID string) *model.AppError
|
||||
DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError
|
||||
DeleteAllExpiredPluginKeys() *model.AppError
|
||||
DeleteAllKeysForPlugin(pluginID string) *model.AppError
|
||||
DeleteBrandImage() *model.AppError
|
||||
DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
|
||||
DeleteCommand(commandID string) *model.AppError
|
||||
DeleteDraft(userID, channelID, rootID, connectionID string) (*model.Draft, *model.AppError)
|
||||
DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError
|
||||
DeleteEphemeralPost(userID, postID string)
|
||||
DeleteExport(name string) *model.AppError
|
||||
@@ -571,6 +572,8 @@ type AppIface interface {
|
||||
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
|
||||
GeneratePublicLink(siteURL string, info *model.FileInfo) string
|
||||
GenerateSupportPacket() []model.FileData
|
||||
GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError)
|
||||
GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError)
|
||||
GetActivePluginManifests() ([]*model.Manifest, *model.AppError)
|
||||
GetAllChannels(c request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError)
|
||||
GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError)
|
||||
@@ -627,6 +630,8 @@ type AppIface interface {
|
||||
GetCustomStatus(userID string) (*model.CustomStatus, *model.AppError)
|
||||
GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError)
|
||||
GetDeletedChannels(c request.CTX, teamID string, offset int, limit int, userID string) (model.ChannelList, *model.AppError)
|
||||
GetDraft(userID, channelID, rootID string) (*model.Draft, *model.AppError)
|
||||
GetDraftsForUser(userID, teamID string) ([]*model.Draft, *model.AppError)
|
||||
GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError)
|
||||
GetEmojiByName(c request.CTX, emojiName string) (*model.Emoji, *model.AppError)
|
||||
GetEmojiImage(c request.CTX, emojiId string) ([]byte, string, *model.AppError)
|
||||
@@ -707,6 +712,7 @@ type AppIface interface {
|
||||
GetPostIdAfterTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError)
|
||||
GetPostIdBeforeTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError)
|
||||
GetPostIfAuthorized(c request.CTX, postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError)
|
||||
GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError)
|
||||
GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError)
|
||||
GetPosts(channelID string, offset int, limit int) (*model.PostList, *model.AppError)
|
||||
GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError)
|
||||
@@ -720,6 +726,8 @@ type AppIface interface {
|
||||
GetPreferenceByCategoryForUser(userID string, category string) (model.Preferences, *model.AppError)
|
||||
GetPreferencesForUser(userID string) (model.Preferences, *model.AppError)
|
||||
GetPrevPostIdFromPostList(postList *model.PostList, collapsedThreads bool) string
|
||||
GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError)
|
||||
GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError)
|
||||
GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError)
|
||||
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
|
||||
GetPublicChannelsByIdsForTeam(c request.CTX, teamID string, channelIDs []string) (model.ChannelList, *model.AppError)
|
||||
@@ -843,6 +851,8 @@ type AppIface interface {
|
||||
GetViewUsersRestrictions(userID string) (*model.ViewUsersRestrictions, *model.AppError)
|
||||
GetWarnMetricsBot() (*model.Bot, *model.AppError)
|
||||
GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError)
|
||||
GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError)
|
||||
GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError)
|
||||
HTTPService() httpservice.HTTPService
|
||||
Handle404(w http.ResponseWriter, r *http.Request)
|
||||
HandleCommandResponse(c request.CTX, command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError)
|
||||
@@ -876,7 +886,6 @@ type AppIface interface {
|
||||
IsLeader() bool
|
||||
IsPasswordValid(password string) *model.AppError
|
||||
IsPhase2MigrationCompleted() *model.AppError
|
||||
IsUserAway(lastActivityAt int64) bool
|
||||
IsUserSignUpAllowed() *model.AppError
|
||||
JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
|
||||
JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError
|
||||
@@ -931,8 +940,8 @@ type AppIface interface {
|
||||
PostUpdateChannelPurposeMessage(c request.CTX, userID string, channel *model.Channel, oldChannelPurpose string, newChannelPurpose string) *model.AppError
|
||||
PostWithProxyAddedToImageURLs(post *model.Post) *model.Post
|
||||
PostWithProxyRemovedFromImageURLs(post *model.Post) *model.Post
|
||||
PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post
|
||||
PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post
|
||||
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
|
||||
ProcessSlackText(text string) string
|
||||
Publish(message *model.WebSocketEvent)
|
||||
@@ -986,6 +995,7 @@ type AppIface interface {
|
||||
SanitizeProfile(user *model.User, asAdmin bool)
|
||||
SanitizeTeam(session model.Session, team *model.Team) *model.Team
|
||||
SanitizeTeams(session model.Session, teams []*model.Team) []*model.Team
|
||||
SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError)
|
||||
SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError
|
||||
SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError)
|
||||
SaveBrandImage(imageData *multipart.FileHeader) *model.AppError
|
||||
@@ -1027,7 +1037,7 @@ type AppIface interface {
|
||||
SendPasswordReset(email string, siteURL string) (bool, *model.AppError)
|
||||
SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError
|
||||
SendTestPushNotification(deviceID string) string
|
||||
SendUpgradeConfirmationEmail() *model.AppError
|
||||
SendUpgradeConfirmationEmail(isYearly bool) *model.AppError
|
||||
ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string)
|
||||
SessionHasPermissionTo(session model.Session, permission *model.Permission) bool
|
||||
SessionHasPermissionToAny(session model.Session, permissions []*model.Permission) bool
|
||||
@@ -1094,6 +1104,7 @@ type AppIface interface {
|
||||
UpdateChannelPrivacy(c request.CTX, oldChannel *model.Channel, user *model.User) (*model.Channel, *model.AppError)
|
||||
UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, *model.AppError)
|
||||
UpdateConfig(f func(*model.Config))
|
||||
UpdateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError)
|
||||
UpdateEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post
|
||||
UpdateExpiredDNDStatuses() ([]*model.Status, error)
|
||||
UpdateGroup(group *model.Group) (*model.Group, *model.AppError)
|
||||
@@ -1101,7 +1112,6 @@ type AppIface interface {
|
||||
UpdateHashedPassword(user *model.User, newHashedPassword string) *model.AppError
|
||||
UpdateHashedPasswordByUserId(userID, newHashedPassword string) *model.AppError
|
||||
UpdateIncomingWebhook(oldHook, updatedHook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError)
|
||||
UpdateLastActivityAtIfNeeded(session model.Session)
|
||||
UpdateMfa(c request.CTX, activate bool, userID, token string) *model.AppError
|
||||
UpdateMobileAppBadge(userID string)
|
||||
UpdateOAuthApp(oldApp, updatedApp *model.OAuthApp) (*model.OAuthApp, *model.AppError)
|
||||
@@ -1139,6 +1149,7 @@ type AppIface interface {
|
||||
UpdateUserRolesWithUser(c request.CTX, user *model.User, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError)
|
||||
UploadData(c *request.Context, us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError)
|
||||
UploadEmojiImage(c request.CTX, id string, imageData *multipart.FileHeader) *model.AppError
|
||||
UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError)
|
||||
UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError)
|
||||
UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError)
|
||||
UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError)
|
||||
|
||||
101
app/channel.go
101
app/channel.go
@@ -343,15 +343,13 @@ func (a *App) CreateChannel(c request.CTX, channel *model.Channel, addMember boo
|
||||
a.InvalidateCacheForUser(channel.CreatorId)
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, sc)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedID)
|
||||
})
|
||||
}
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, sc)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedID)
|
||||
})
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
@@ -429,15 +427,13 @@ func (a *App) handleCreationEvent(c request.CTX, userID, otherUserID string, cha
|
||||
a.InvalidateCacheForUser(userID)
|
||||
a.InvalidateCacheForUser(otherUserID)
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, channel)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedID)
|
||||
})
|
||||
}
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, channel)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedID)
|
||||
})
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventDirectAdded, "", channel.Id, "", nil, "")
|
||||
message.Add("creator_id", userID)
|
||||
@@ -1599,15 +1595,13 @@ func (a *App) AddChannelMember(c request.CTX, userID string, channel *model.Chan
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelID)
|
||||
})
|
||||
}
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelID)
|
||||
})
|
||||
|
||||
if opts.UserRequestorID == "" || userID == opts.UserRequestorID {
|
||||
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
|
||||
@@ -2177,15 +2171,13 @@ func (a *App) JoinChannel(c request.CTX, channel *model.Channel, userID string)
|
||||
return err
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelID)
|
||||
})
|
||||
}
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelID)
|
||||
})
|
||||
|
||||
if err := a.postJoinChannelMessage(c, user, channel); err != nil {
|
||||
return err
|
||||
@@ -2484,21 +2476,19 @@ func (a *App) removeUserFromChannel(c request.CTX, userIDToRemove string, remove
|
||||
a.InvalidateCacheForUser(userIDToRemove)
|
||||
a.invalidateCacheForChannelMembers(channel.Id)
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var actorUser *model.User
|
||||
if removerUserId != "" {
|
||||
actorUser, _ = a.GetUser(removerUserId)
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
|
||||
return true
|
||||
}, plugin.UserHasLeftChannelID)
|
||||
})
|
||||
var actorUser *model.User
|
||||
if removerUserId != "" {
|
||||
actorUser, _ = a.GetUser(removerUserId)
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
|
||||
return true
|
||||
}, plugin.UserHasLeftChannelID)
|
||||
})
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventUserRemoved, "", channel.Id, "", nil, "")
|
||||
message.Add("user_id", userIDToRemove)
|
||||
message.Add("remover_id", removerUserId)
|
||||
@@ -2609,12 +2599,12 @@ func (a *App) MarkChannelAsUnreadFromPost(c request.CTX, postID string, userID s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unreadMentions, unreadMentionsRoot, err := a.countMentionsFromPost(c, user, post)
|
||||
unreadMentions, unreadMentionsRoot, urgentMentions, err := a.countMentionsFromPost(c, user, post)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, true)
|
||||
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
|
||||
if nErr != nil {
|
||||
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
@@ -2641,7 +2631,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st
|
||||
threadId = post.Id
|
||||
}
|
||||
|
||||
unreadMentions, unreadMentionsRoot, appErr := a.countMentionsFromPost(c, user, post)
|
||||
unreadMentions, unreadMentionsRoot, urgentMentions, appErr := a.countMentionsFromPost(c, user, post)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
@@ -2650,7 +2640,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st
|
||||
// In CRT Supported Client: badge on channel only sums mentions in root posts including and below the post that was marked.
|
||||
// In CRT Unsupported Client: badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread.
|
||||
if post.RootId == "" {
|
||||
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, true)
|
||||
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, unreadMentionsRoot, urgentMentions, true)
|
||||
if nErr != nil {
|
||||
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
@@ -2706,7 +2696,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st
|
||||
if mErr != nil {
|
||||
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
|
||||
}
|
||||
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true)
|
||||
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
|
||||
if mErr != nil {
|
||||
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
|
||||
}
|
||||
@@ -2724,7 +2714,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st
|
||||
}
|
||||
}
|
||||
|
||||
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, false)
|
||||
channelUnread, nErr := a.Srv().Store().Channel().UpdateLastViewedAtPost(post, userID, unreadMentions, 0, 0, false)
|
||||
if nErr != nil {
|
||||
return channelUnread, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
@@ -2741,6 +2731,7 @@ func (a *App) sendWebSocketPostUnreadEvent(c request.CTX, channelUnread *model.C
|
||||
}
|
||||
message.Add("mention_count", channelUnread.MentionCount)
|
||||
message.Add("mention_count_root", channelUnread.MentionCountRoot)
|
||||
message.Add("urgent_mention_count", channelUnread.UrgentMentionCount)
|
||||
message.Add("last_viewed_at", channelUnread.LastViewedAt)
|
||||
message.Add("post_id", postID)
|
||||
a.Publish(message)
|
||||
|
||||
@@ -322,5 +322,33 @@ func (s *hooksService) RegisterHooks(productID string, hooks any) error {
|
||||
return errors.New("could not find plugins environment")
|
||||
}
|
||||
|
||||
return s.ch.pluginsEnvironment.AddProduct(productID, hooks)
|
||||
return s.ch.srv.hooksManager.AddProduct(productID, hooks)
|
||||
}
|
||||
|
||||
func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
|
||||
if env := ch.GetPluginsEnvironment(); env != nil {
|
||||
env.RunMultiPluginHook(hookRunnerFunc, hookId)
|
||||
}
|
||||
|
||||
// run hook for the products
|
||||
ch.srv.hooksManager.RunMultiHook(hookRunnerFunc, hookId)
|
||||
}
|
||||
|
||||
func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) {
|
||||
var hooks plugin.Hooks
|
||||
if env := ch.GetPluginsEnvironment(); env != nil {
|
||||
// we intentionally ignore the error here, because the id can be a product id
|
||||
// we are going to check if we have the hooks or not
|
||||
hooks, _ = env.HooksForPlugin(id)
|
||||
if hooks != nil {
|
||||
return hooks, nil
|
||||
}
|
||||
}
|
||||
|
||||
hooks = ch.srv.hooksManager.HooksForProduct(id)
|
||||
if hooks != nil {
|
||||
return hooks, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find hooks for id %s", id)
|
||||
}
|
||||
|
||||
21
app/cloud.go
21
app/cloud.go
@@ -174,7 +174,7 @@ func getNextBillingDateString() string {
|
||||
return fmt.Sprintf("%s %d, %d", t.Month(), t.Day(), t.Year())
|
||||
}
|
||||
|
||||
func (a *App) SendUpgradeConfirmationEmail() *model.AppError {
|
||||
func (a *App) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError {
|
||||
sysAdmins, e := a.getSysAdminsEmailRecipients()
|
||||
if e != nil {
|
||||
return e
|
||||
@@ -200,7 +200,7 @@ func (a *App) SendUpgradeConfirmationEmail() *model.AppError {
|
||||
name = admin.Username
|
||||
}
|
||||
|
||||
err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS())
|
||||
err := a.Srv().EmailService.SendCloudUpgradeConfirmationEmail(admin.Email, name, billingDate, admin.Locale, *a.Config().ServiceSettings.SiteURL, subscription.GetWorkSpaceNameFromDNS(), isYearly)
|
||||
if err != nil {
|
||||
a.Log().Error("Error sending trial ended email to", mlog.String("email", admin.Email), mlog.Err(err))
|
||||
countNotOks++
|
||||
@@ -230,3 +230,20 @@ func (a *App) SendNoCardPaymentFailedEmail() *model.AppError {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create/ Update a subscription history event
|
||||
func (a *App) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
|
||||
license := a.Srv().License()
|
||||
|
||||
// No need to create a Subscription History Event if the license isn't cloud
|
||||
if !license.IsCloud() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get user count
|
||||
userCount, err := a.Srv().Store().User().Count(model.UserCountOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.Cloud().CreateOrUpdateSubscriptionHistoryEvent(userID, int(userCount))
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ func (s *Server) clusterRemovePluginHandler(msg *model.ClusterMessage) {
|
||||
}
|
||||
|
||||
func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {
|
||||
env := s.Channels().GetPluginsEnvironment()
|
||||
if env == nil {
|
||||
return
|
||||
}
|
||||
if msg.Props == nil {
|
||||
mlog.Warn("ClusterMessage.Props for plugin event should not be nil")
|
||||
return
|
||||
}
|
||||
pluginID := msg.Props["PluginID"]
|
||||
// if the plugin key is empty, the message might be coming from a product.
|
||||
if pluginID == "" {
|
||||
pluginID = msg.Props["ProductID"]
|
||||
}
|
||||
eventID := msg.Props["EventID"]
|
||||
if pluginID == "" || eventID == "" {
|
||||
mlog.Warn("Invalid ClusterMessage.Props values for plugin event",
|
||||
@@ -44,7 +44,12 @@ func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
hooks, err := env.HooksForPlugin(pluginID)
|
||||
channels, ok := s.products["channels"].(*Channels)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
hooks, err := channels.HooksForPluginOrProduct(pluginID)
|
||||
if err != nil {
|
||||
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
|
||||
return
|
||||
|
||||
211
app/draft.go
Обычный файл
211
app/draft.go
Обычный файл
@@ -0,0 +1,211 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
func (a *App) GetDraft(userID, channelID, rootID string) (*model.Draft, *model.AppError) {
|
||||
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
return nil, model.NewAppError("GetDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
draft, err := a.Srv().Store().Draft().Get(userID, channelID, rootID, false)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("GetDraft", "app.draft.get.app_error", nil, err.Error(), http.StatusNotFound)
|
||||
default:
|
||||
return nil, model.NewAppError("GetDraft", "app.draft.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
return draft, nil
|
||||
}
|
||||
|
||||
func (a *App) UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
|
||||
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
return nil, model.NewAppError("UpsertDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
dt, dErr := a.Srv().Store().Draft().Get(draft.UserId, draft.ChannelId, draft.RootId, true)
|
||||
var notFoundErr *store.ErrNotFound
|
||||
if dErr != nil && !errors.As(dErr, ¬FoundErr) {
|
||||
return nil, model.NewAppError("UpsertDraft", "app.select_error", nil, dErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
var err *model.AppError
|
||||
if dt == nil {
|
||||
dt, err = a.CreateDraft(c, draft, connectionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
dt, err = a.UpdateDraft(c, draft, connectionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return dt, nil
|
||||
}
|
||||
|
||||
func (a *App) CreateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
|
||||
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
return nil, model.NewAppError("CreateDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// Check that channel exists and has not been deleted
|
||||
channel, errCh := a.Srv().Store().Channel().Get(draft.ChannelId, true)
|
||||
if errCh != nil {
|
||||
err := model.NewAppError("CreateDraft", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "draft.channel_id"}, errCh.Error(), http.StatusBadRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if channel.DeleteAt != 0 {
|
||||
err := model.NewAppError("CreateDraft", "api.draft.create_draft.can_not_draft_to_deleted.error", nil, "", http.StatusBadRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, nErr := a.Srv().Store().User().Get(context.Background(), draft.UserId)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("CreateDraft", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
dt, nErr := a.Srv().Store().Draft().Save(draft)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("CreateDraft", "app.draft.save.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
dt = a.prepareDraftWithFileInfos(draft.UserId, dt)
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventDraftCreated, "", dt.ChannelId, dt.UserId, nil, connectionID)
|
||||
draftJSON, jsonErr := json.Marshal(dt)
|
||||
if jsonErr != nil {
|
||||
mlog.Warn("Failed to encode draft to JSON", mlog.Err(jsonErr))
|
||||
}
|
||||
message.Add("draft", string(draftJSON))
|
||||
a.Publish(message)
|
||||
|
||||
return dt, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
|
||||
if !a.Config().FeatureFlags.GlobalDrafts {
|
||||
return nil, model.NewAppError("UpsertDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// Check that channel exists and has not been deleted
|
||||
channel, errCh := a.Srv().Store().Channel().Get(draft.ChannelId, true)
|
||||
if errCh != nil {
|
||||
err := model.NewAppError("UpdateDraft", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "draft.channel_id"}, errCh.Error(), http.StatusBadRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if channel.DeleteAt != 0 {
|
||||
err := model.NewAppError("UpdateDraft", "api.draft.create_draft.can_not_draft_to_deleted.error", nil, "", http.StatusBadRequest)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, nErr := a.Srv().Store().User().Get(context.Background(), draft.UserId)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("UpdateDraft", "app.user.get.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
dt, nErr := a.Srv().Store().Draft().Update(draft)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("UpdateDraft", "app.draft.update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
dt = a.prepareDraftWithFileInfos(draft.UserId, dt)
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventDraftUpdated, "", draft.ChannelId, draft.UserId, nil, connectionID)
|
||||
draftJSON, jsonErr := json.Marshal(dt)
|
||||
if jsonErr != nil {
|
||||
mlog.Warn("Failed to encode draft to JSON", mlog.Err(jsonErr))
|
||||
}
|
||||
message.Add("draft", string(draftJSON))
|
||||
a.Publish(message)
|
||||
|
||||
return dt, nil
|
||||
}
|
||||
|
||||
func (a *App) GetDraftsForUser(userID, teamID string) ([]*model.Draft, *model.AppError) {
|
||||
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
return nil, model.NewAppError("GetDraftsForUser", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
drafts, err := a.Srv().Store().Draft().GetDraftsForUser(userID, teamID)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetDraftsForUser", "app.draft.get_drafts.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, draft := range drafts {
|
||||
a.prepareDraftWithFileInfos(userID, draft)
|
||||
}
|
||||
return drafts, nil
|
||||
}
|
||||
|
||||
func (a *App) prepareDraftWithFileInfos(userID string, draft *model.Draft) *model.Draft {
|
||||
if fileInfos, err := a.getFileInfosForDraft(draft); err != nil {
|
||||
mlog.Error("Failed to get files for a user's drafts", mlog.String("user_id", userID), mlog.Err(err))
|
||||
} else {
|
||||
draft.Metadata = &model.PostMetadata{}
|
||||
draft.Metadata.Files = fileInfos
|
||||
}
|
||||
|
||||
return draft
|
||||
}
|
||||
|
||||
func (a *App) getFileInfosForDraft(draft *model.Draft) ([]*model.FileInfo, *model.AppError) {
|
||||
if len(draft.FileIds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fileInfos, err := a.Srv().Store().FileInfo().GetByIds(draft.FileIds)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetFileInfosForDraft", "app.draft.get_for_draft.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
a.generateMiniPreviewForInfos(fileInfos)
|
||||
|
||||
return fileInfos, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteDraft(userID, channelID, rootID, connectionID string) (*model.Draft, *model.AppError) {
|
||||
if !a.Config().FeatureFlags.GlobalDrafts || !*a.Config().ServiceSettings.AllowSyncedDrafts {
|
||||
return nil, model.NewAppError("DeleteDraft", "app.draft.feature_disabled", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
draft, nErr := a.Srv().Store().Draft().Get(userID, channelID, rootID, false)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("DeleteDraft", "app.draft.get.app_error", nil, nErr.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.Srv().Store().Draft().Delete(userID, channelID, rootID); err != nil {
|
||||
return nil, model.NewAppError("DeleteDraft", "app.draft.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
draftJSON, jsonErr := json.Marshal(draft)
|
||||
if jsonErr != nil {
|
||||
mlog.Warn("Failed to encode draft to JSON")
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventDraftDeleted, "", draft.ChannelId, draft.UserId, nil, connectionID)
|
||||
message.Add("draft", string(draftJSON))
|
||||
a.Publish(message)
|
||||
|
||||
return draft, nil
|
||||
}
|
||||
428
app/draft_test.go
Обычный файл
428
app/draft_test.go
Обычный файл
@@ -0,0 +1,428 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/utils/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDraft(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
|
||||
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "true")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.platform.SetConfigReadOnlyFF(false)
|
||||
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
user := th.BasicUser
|
||||
channel := th.BasicChannel
|
||||
|
||||
draft := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft",
|
||||
}
|
||||
|
||||
_, upsertDraftErr := th.App.UpsertDraft(th.Context, draft, "")
|
||||
assert.Nil(t, upsertDraftErr)
|
||||
|
||||
t.Run("get draft", func(t *testing.T) {
|
||||
draftResp, err := th.App.GetDraft(user.Id, channel.Id, "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draft.Message, draftResp.Message)
|
||||
assert.Equal(t, draft.ChannelId, draftResp.ChannelId)
|
||||
})
|
||||
|
||||
t.Run("get draft feature flag", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
|
||||
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
_, err := th.App.GetDraft(user.Id, channel.Id, "")
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpsertDraft(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.platform.SetConfigReadOnlyFF(false)
|
||||
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
user := th.BasicUser
|
||||
channel := th.BasicChannel
|
||||
|
||||
draft1 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft1",
|
||||
}
|
||||
|
||||
draft2 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00002,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft2",
|
||||
}
|
||||
|
||||
_, createDraftErr := th.App.CreateDraft(th.Context, draft1, "")
|
||||
assert.Nil(t, createDraftErr)
|
||||
|
||||
t.Run("upsert draft", func(t *testing.T) {
|
||||
draftResp, err := th.App.UpsertDraft(th.Context, draft2, "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draft2.Message, draftResp.Message)
|
||||
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
|
||||
assert.Equal(t, draft2.CreateAt, draftResp.CreateAt)
|
||||
|
||||
assert.NotEqual(t, draft1.UpdateAt, draftResp.UpdateAt)
|
||||
})
|
||||
|
||||
t.Run("upsert draft feature flag", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
|
||||
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
_, err := th.App.UpsertDraft(th.Context, draft1, "")
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDraft(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.platform.SetConfigReadOnlyFF(false)
|
||||
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
user := th.BasicUser
|
||||
channel := th.BasicChannel
|
||||
channel2 := th.CreateChannel(th.Context, th.BasicTeam)
|
||||
th.AddUserToChannel(user, channel2)
|
||||
|
||||
draft1 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft",
|
||||
}
|
||||
|
||||
draft2 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel2.Id,
|
||||
Message: "draft2",
|
||||
}
|
||||
|
||||
t.Run("create draft", func(t *testing.T) {
|
||||
draftResp, err := th.App.CreateDraft(th.Context, draft1, "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draft1.Message, draftResp.Message)
|
||||
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
|
||||
})
|
||||
|
||||
t.Run("create draft with files", func(t *testing.T) {
|
||||
// upload file
|
||||
sent, readFileErr := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, readFileErr)
|
||||
|
||||
fileResp, uploadFileErr := th.App.UploadFile(th.Context, sent, channel.Id, "test.png")
|
||||
assert.Nil(t, uploadFileErr)
|
||||
|
||||
draftWithFiles := draft2
|
||||
draftWithFiles.FileIds = []string{fileResp.Id}
|
||||
|
||||
draftResp, err := th.App.CreateDraft(th.Context, draftWithFiles, "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draftWithFiles.Message, draftResp.Message)
|
||||
assert.Equal(t, draftWithFiles.ChannelId, draftResp.ChannelId)
|
||||
assert.ElementsMatch(t, draftWithFiles.FileIds, draftResp.FileIds)
|
||||
})
|
||||
|
||||
t.Run("create draft feature flag", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
|
||||
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
_, err := th.App.CreateDraft(th.Context, draft1, "")
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateDraft(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.platform.SetConfigReadOnlyFF(false)
|
||||
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
user := th.BasicUser
|
||||
channel := th.BasicChannel
|
||||
|
||||
draft1 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft1",
|
||||
}
|
||||
|
||||
draft2 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00002,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft2",
|
||||
}
|
||||
|
||||
_, createDraftErr := th.App.CreateDraft(th.Context, draft1, "")
|
||||
assert.Nil(t, createDraftErr)
|
||||
|
||||
t.Run("update draft", func(t *testing.T) {
|
||||
draftResp, err := th.App.UpdateDraft(th.Context, draft2, "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draft2.Message, draftResp.Message)
|
||||
assert.Equal(t, draft2.ChannelId, draftResp.ChannelId)
|
||||
|
||||
assert.NotEqual(t, draft1.UpdateAt, draftResp.UpdateAt)
|
||||
})
|
||||
|
||||
t.Run("update draft with files", func(t *testing.T) {
|
||||
// upload file
|
||||
sent, readFileErr := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, readFileErr)
|
||||
|
||||
fileResp, uploadFileErr := th.App.UploadFile(th.Context, sent, channel.Id, "test.png")
|
||||
assert.Nil(t, uploadFileErr)
|
||||
|
||||
draftWithFiles := draft1
|
||||
draftWithFiles.FileIds = []string{fileResp.Id}
|
||||
|
||||
draftResp, err := th.App.UpdateDraft(th.Context, draft1, "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draftWithFiles.Message, draftResp.Message)
|
||||
assert.Equal(t, draftWithFiles.ChannelId, draftResp.ChannelId)
|
||||
assert.ElementsMatch(t, draftWithFiles.FileIds, draftResp.FileIds)
|
||||
})
|
||||
|
||||
t.Run("create draft feature flag", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
|
||||
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
_, err := th.App.UpdateDraft(th.Context, draft1, "")
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDraftsForUser(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.platform.SetConfigReadOnlyFF(false)
|
||||
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
user := th.BasicUser
|
||||
channel := th.BasicChannel
|
||||
channel2 := th.CreateChannel(th.Context, th.BasicTeam)
|
||||
th.AddUserToChannel(user, channel2)
|
||||
|
||||
draft1 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft1",
|
||||
}
|
||||
|
||||
draft2 := &model.Draft{
|
||||
CreateAt: 00005,
|
||||
UpdateAt: 00005,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel2.Id,
|
||||
Message: "draft2",
|
||||
}
|
||||
|
||||
_, createDraftErr1 := th.App.CreateDraft(th.Context, draft1, "")
|
||||
assert.Nil(t, createDraftErr1)
|
||||
|
||||
_, createDraftErr2 := th.App.CreateDraft(th.Context, draft2, "")
|
||||
assert.Nil(t, createDraftErr2)
|
||||
|
||||
t.Run("get drafts", func(t *testing.T) {
|
||||
draftResp, err := th.App.GetDraftsForUser(user.Id, th.BasicTeam.Id)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draft2.Message, draftResp[0].Message)
|
||||
assert.Equal(t, draft2.ChannelId, draftResp[0].ChannelId)
|
||||
|
||||
assert.Equal(t, draft1.Message, draftResp[1].Message)
|
||||
assert.Equal(t, draft1.ChannelId, draftResp[1].ChannelId)
|
||||
})
|
||||
|
||||
t.Run("get drafts with files", func(t *testing.T) {
|
||||
// upload file
|
||||
sent, readFileErr := testutils.ReadTestFile("test.png")
|
||||
require.NoError(t, readFileErr)
|
||||
|
||||
fileResp, updateDraftErr := th.App.UploadFile(th.Context, sent, channel.Id, "test.png")
|
||||
assert.Nil(t, updateDraftErr)
|
||||
|
||||
draftWithFiles := draft1
|
||||
draftWithFiles.FileIds = []string{fileResp.Id}
|
||||
|
||||
draftResp, updateDraftErr := th.App.UpdateDraft(th.Context, draft1, "")
|
||||
assert.Nil(t, updateDraftErr)
|
||||
|
||||
assert.Equal(t, draftWithFiles.Message, draftResp.Message)
|
||||
assert.Equal(t, draftWithFiles.ChannelId, draftResp.ChannelId)
|
||||
assert.ElementsMatch(t, draftWithFiles.FileIds, draftResp.FileIds)
|
||||
|
||||
draftsWithFilesResp, err := th.App.GetDraftsForUser(user.Id, th.BasicTeam.Id)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draftWithFiles.Message, draftsWithFilesResp[0].Message)
|
||||
assert.Equal(t, draftWithFiles.ChannelId, draftsWithFilesResp[0].ChannelId)
|
||||
assert.ElementsMatch(t, draftWithFiles.FileIds, draftsWithFilesResp[0].FileIds)
|
||||
|
||||
assert.Equal(t, fileResp.Name, draftsWithFilesResp[0].Metadata.Files[0].Name)
|
||||
|
||||
assert.Len(t, draftsWithFilesResp, 2)
|
||||
})
|
||||
|
||||
t.Run("get drafts feature flag", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
|
||||
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
_, err := th.App.GetDraftsForUser(user.Id, th.BasicTeam.Id)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteDraft(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Server.platform.SetConfigReadOnlyFF(false)
|
||||
defer th.Server.platform.SetConfigReadOnlyFF(true)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
user := th.BasicUser
|
||||
channel := th.BasicChannel
|
||||
|
||||
draft1 := &model.Draft{
|
||||
CreateAt: 00001,
|
||||
UpdateAt: 00001,
|
||||
UserId: user.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "draft1",
|
||||
}
|
||||
|
||||
_, createDraftErr := th.App.CreateDraft(th.Context, draft1, "")
|
||||
assert.Nil(t, createDraftErr)
|
||||
|
||||
t.Run("delete draft", func(t *testing.T) {
|
||||
draftResp, err := th.App.DeleteDraft(user.Id, channel.Id, "", "")
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, draft1.Message, draftResp.Message)
|
||||
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
|
||||
|
||||
assert.Equal(t, draft1.Message, draftResp.Message)
|
||||
assert.Equal(t, draft1.ChannelId, draftResp.ChannelId)
|
||||
})
|
||||
|
||||
t.Run("get drafts feature flag", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GLOBALDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GLOBALDRAFTS")
|
||||
os.Setenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS", "false")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ALLOWSYNCEDDRAFTS")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = false })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = false })
|
||||
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.GlobalDrafts = true })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowSyncedDrafts = true })
|
||||
|
||||
_, err := th.App.DeleteDraft(user.Id, channel.Id, "", "")
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
@@ -233,13 +233,13 @@ func (es *Service) SendWelcomeEmail(userID string, email string, verified bool,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string) error {
|
||||
func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, locale, siteURL, workspaceName string, isYearly bool) error {
|
||||
T := i18n.GetUserTranslations(locale)
|
||||
subject := T("api.templates.cloud_upgrade_confirmation.subject")
|
||||
|
||||
data := es.NewEmailTemplateData(locale)
|
||||
data.Props["Title"] = T("api.templates.cloud_upgrade_confirmation.title")
|
||||
data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date})
|
||||
data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_monthly.subtitle", map[string]any{"WorkspaceName": workspaceName, "Date": date})
|
||||
data.Props["SiteURL"] = siteURL
|
||||
data.Props["ButtonURL"] = siteURL
|
||||
data.Props["Button"] = T("api.templates.cloud_welcome_email.button")
|
||||
@@ -247,6 +247,12 @@ func (es *Service) SendCloudUpgradeConfirmationEmail(userEmail, name, date, loca
|
||||
data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
|
||||
data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
|
||||
|
||||
if isYearly {
|
||||
data.Props["SubTitle"] = T("api.templates.cloud_upgrade_confirmation_yearly.subtitle", map[string]any{"WorkspaceName": workspaceName})
|
||||
data.Props["ButtonURL"] = siteURL + "/admin_console/billing/billing_history"
|
||||
data.Props["Button"] = T("api.templates.cloud_welcome_email.yearly_plan_button")
|
||||
}
|
||||
|
||||
body, err := es.templatesContainer.RenderToString("cloud_upgrade_confirmation", data)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1037,7 +1043,7 @@ func (es *Service) SendNoCardPaymentFailedEmail(email string, locale string, sit
|
||||
func (es *Service) SendDelinquencyEmail7(email, locale, siteURL, planName string) error {
|
||||
T := i18n.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.payment_failed.subject")
|
||||
subject := T("api.templates.payment_failed.subject", map[string]any{"Plan": planName})
|
||||
|
||||
data := es.NewEmailTemplateData(locale)
|
||||
data.Props["SiteURL"] = siteURL
|
||||
|
||||
@@ -33,6 +33,7 @@ type postData struct {
|
||||
Time string
|
||||
ShowChannelIcon bool
|
||||
OtherChannelMembersCount int
|
||||
MessageAttachments []*EmailMessageAttachment
|
||||
}
|
||||
|
||||
func (es *Service) InitEmailBatching() {
|
||||
@@ -314,6 +315,7 @@ func (es *Service) sendBatchedEmailNotification(userID string, notifications []*
|
||||
MessageURL: MessageURL,
|
||||
ShowChannelIcon: showChannelIcon,
|
||||
OtherChannelMembersCount: otherChannelMembersCount,
|
||||
MessageAttachments: ProcessMessageAttachments(notification.post),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ func TestSendCloudUpgradedEmail(t *testing.T) {
|
||||
emailTo := "testclouduser@example.com"
|
||||
emailToUsername := strings.Split(emailTo, "@")[0]
|
||||
|
||||
t.Run("SendCloudUpgradedEmail", func(t *testing.T) {
|
||||
t.Run("SendCloudMonthlyUpgradedEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
@@ -278,10 +278,44 @@ func TestSendCloudUpgradedEmail(t *testing.T) {
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "You'll be billed from", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "Open Mattermost", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName")
|
||||
// Send Update to Monthly Plan email
|
||||
err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
|
||||
t.Run("SendCloudYearlyUpgradedEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "View your invoice", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
// Send Update to Monthly Plan email
|
||||
err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
|
||||
@@ -125,13 +125,13 @@ func (_m *ServiceInterface) SendChangeUsernameEmail(newUsername string, _a1 stri
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName
|
||||
func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string) error {
|
||||
ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName)
|
||||
// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly
|
||||
func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string, isYearly bool) error {
|
||||
ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) error); ok {
|
||||
r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName)
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, bool) error); ok {
|
||||
r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"html"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -11,8 +13,21 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
type FieldRow struct {
|
||||
Cells []*model.SlackAttachmentField
|
||||
}
|
||||
|
||||
type EmailMessageAttachment struct {
|
||||
model.SlackAttachment
|
||||
|
||||
Pretext template.HTML
|
||||
Text template.HTML
|
||||
FieldRows []FieldRow
|
||||
}
|
||||
|
||||
func (es *Service) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
|
||||
if strings.TrimSpace(post.Message) != "" || len(post.FileIds) == 0 {
|
||||
return post.Message
|
||||
@@ -44,3 +59,78 @@ func (es *Service) GetMessageForNotification(post *model.Post, translateFunc i18
|
||||
}
|
||||
return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
|
||||
}
|
||||
|
||||
func ProcessMessageAttachments(post *model.Post) []*EmailMessageAttachment {
|
||||
emailMessageAttachments := []*EmailMessageAttachment{}
|
||||
|
||||
for _, messageAttachment := range post.Attachments() {
|
||||
emailMessageAttachment := &EmailMessageAttachment{
|
||||
SlackAttachment: *messageAttachment,
|
||||
Pretext: prepareTextForEmail(messageAttachment.Pretext),
|
||||
Text: prepareTextForEmail(messageAttachment.Text),
|
||||
}
|
||||
|
||||
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
|
||||
if err != nil {
|
||||
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
stripedTitle = ""
|
||||
}
|
||||
|
||||
emailMessageAttachment.Title = stripedTitle
|
||||
|
||||
shortFieldRow := FieldRow{}
|
||||
|
||||
for i := range messageAttachment.Fields {
|
||||
// Create a new instance to avoid altering the original pointer reference
|
||||
// We update field value to parse markdown.
|
||||
// If we do that on the original pointer, the rendered text in mattermost
|
||||
// becomes invalid as its no longer a markdown string, but rather an HTML string.
|
||||
field := &model.SlackAttachmentField{
|
||||
Title: messageAttachment.Fields[i].Title,
|
||||
Value: messageAttachment.Fields[i].Value,
|
||||
Short: messageAttachment.Fields[i].Short,
|
||||
}
|
||||
|
||||
if stringValue, ok := field.Value.(string); ok {
|
||||
field.Value = prepareTextForEmail(stringValue)
|
||||
}
|
||||
|
||||
if !field.Short {
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
|
||||
} else {
|
||||
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
|
||||
|
||||
if len(shortFieldRow.Cells) == 2 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect any leftover short fields
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
|
||||
}
|
||||
|
||||
return emailMessageAttachments
|
||||
}
|
||||
|
||||
func prepareTextForEmail(text string) template.HTML {
|
||||
escapedText := html.EscapeString(text)
|
||||
markdownText, err := utils.MarkdownToHTML(escapedText)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
|
||||
return template.HTML(text)
|
||||
}
|
||||
|
||||
return template.HTML(markdownText)
|
||||
}
|
||||
|
||||
72
app/email/notification_email_test.go
Обычный файл
72
app/email/notification_email_test.go
Обычный файл
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProcessMessageAttachments(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
post := &model.Post{
|
||||
Message: "This is the message",
|
||||
}
|
||||
|
||||
messageAttachments := []*model.SlackAttachment{
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 1 pretext",
|
||||
AuthorName: "author name",
|
||||
AuthorLink: "https://example.com/slack_attachment_1/author_link",
|
||||
AuthorIcon: "https://example.com/slack_attachment_1/author_icon",
|
||||
Title: "message attachment 1 title",
|
||||
TitleLink: "https://example.com/slack_attachment_1/title_link",
|
||||
Text: "message attachment 1 text",
|
||||
ImageURL: "https://example.com/slack_attachment_1/image",
|
||||
ThumbURL: "https://example.com/slack_attachment_1/thumb",
|
||||
Fields: []*model.SlackAttachmentField{
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 1 title",
|
||||
Value: "message attachment 1 field 1 value",
|
||||
},
|
||||
{
|
||||
Short: false,
|
||||
Title: "message attachment 1 field 2 title",
|
||||
Value: "message attachment 1 field 2 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 3 title",
|
||||
Value: "message attachment 1 field 3 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 4 title",
|
||||
Value: "message attachment 1 field 4 value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 2 pretext",
|
||||
AuthorName: "author name 2",
|
||||
Text: "message attachment 2 text",
|
||||
},
|
||||
}
|
||||
|
||||
model.ParseSlackAttachment(post, messageAttachments)
|
||||
|
||||
processedAttachcmentsPost := ProcessMessageAttachments(post)
|
||||
require.NotNil(t, processedAttachcmentsPost)
|
||||
require.Len(t, processedAttachcmentsPost, 2)
|
||||
require.Equal(t, processedAttachcmentsPost[0].Color, "#FF0000")
|
||||
require.Equal(t, processedAttachcmentsPost[0].FieldRows[0].Cells[0].Title, "message attachment 1 field 1 title")
|
||||
require.Equal(t, processedAttachcmentsPost[1].Color, "#FF0000")
|
||||
}
|
||||
@@ -129,7 +129,7 @@ type ServiceInterface interface {
|
||||
SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error
|
||||
SendSignInChangeEmail(email, method, locale, siteURL string) error
|
||||
SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error
|
||||
SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string) error
|
||||
SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string, isYearly bool) error
|
||||
SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error
|
||||
SendPasswordChangeEmail(email, method, locale, siteURL string) error
|
||||
SendUserAccessTokenAddedEmail(email, locale, siteURL string) error
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -157,9 +159,17 @@ func (a *App) exportWriteLine(w io.Writer, line *imports.LineImportData) *model.
|
||||
|
||||
func (a *App) exportVersion(writer io.Writer) *model.AppError {
|
||||
version := 1
|
||||
|
||||
info := &imports.VersionInfoImportData{
|
||||
Generator: "mattermost-server",
|
||||
Version: fmt.Sprintf("%s (%s, enterprise: %s)", model.CurrentVersion, model.BuildHash, model.BuildEnterpriseReady),
|
||||
Created: time.Now().Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
versionLine := &imports.LineImportData{
|
||||
Type: "version",
|
||||
Version: &version,
|
||||
Info: info,
|
||||
}
|
||||
|
||||
return a.exportWriteLine(writer, versionLine)
|
||||
|
||||
64
app/file.go
64
app/file.go
@@ -895,29 +895,27 @@ func (a *App) DoUploadFileExpectModification(c request.CTX, now time.Time, rawTe
|
||||
info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb." + getFileExtFromMimeType(info.MimeType)
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var rejectionError *model.AppError
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
var newBytes bytes.Buffer
|
||||
replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes)
|
||||
if rejectionReason != "" {
|
||||
rejectionError = model.NewAppError("DoUploadFile", "File rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if replacementInfo != nil {
|
||||
info = replacementInfo
|
||||
}
|
||||
if newBytes.Len() != 0 {
|
||||
data = newBytes.Bytes()
|
||||
info.Size = int64(len(data))
|
||||
}
|
||||
|
||||
return true
|
||||
}, plugin.FileWillBeUploadedID)
|
||||
if rejectionError != nil {
|
||||
return nil, data, rejectionError
|
||||
var rejectionError *model.AppError
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
var newBytes bytes.Buffer
|
||||
replacementInfo, rejectionReason := hooks.FileWillBeUploaded(pluginContext, info, bytes.NewReader(data), &newBytes)
|
||||
if rejectionReason != "" {
|
||||
rejectionError = model.NewAppError("DoUploadFile", "File rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if replacementInfo != nil {
|
||||
info = replacementInfo
|
||||
}
|
||||
if newBytes.Len() != 0 {
|
||||
data = newBytes.Bytes()
|
||||
info.Size = int64(len(data))
|
||||
}
|
||||
|
||||
return true
|
||||
}, plugin.FileWillBeUploadedID)
|
||||
if rejectionError != nil {
|
||||
return nil, data, rejectionError
|
||||
}
|
||||
|
||||
if _, err := a.WriteFile(bytes.NewReader(data), info.Path); err != nil {
|
||||
@@ -1380,6 +1378,28 @@ func (a *App) ComputeLastAccessibleFileTime() error {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
// All files are accessible - we must check if a previous value was set so we can clear it
|
||||
systemValue, err := a.Srv().Store().System().GetByName(model.SystemLastAccessibleFileTime)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
// All files are already accessible
|
||||
return nil
|
||||
default:
|
||||
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.get_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
if systemValue != nil {
|
||||
// Previous value was set, so we must clear it
|
||||
if _, err := a.Srv().Store().System().PermanentDeleteByName(model.SystemLastAccessibleFileTime); err != nil {
|
||||
return model.NewAppError("ComputeLastAccessibleFileTime", "app.system.permanent_delete_by_name.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
createdAt, err := a.Srv().GetStore().FileInfo().GetUptoNSizeFileTime(limit)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
|
||||
@@ -591,30 +591,61 @@ func TestGetLastAccessibleFileTime(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestComputeLastAccessibleFileTime(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
t.Run("Updates the time, if cloud limit is applicable", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := &eMocks.CloudInterface{}
|
||||
th.App.Srv().Cloud = cloud
|
||||
cloud := &eMocks.CloudInterface{}
|
||||
th.App.Srv().Cloud = cloud
|
||||
|
||||
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(&model.ProductLimits{
|
||||
Files: &model.FilesLimits{
|
||||
TotalStorage: model.NewInt64(1),
|
||||
},
|
||||
}, nil)
|
||||
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(&model.ProductLimits{
|
||||
Files: &model.FilesLimits{
|
||||
TotalStorage: model.NewInt64(1),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockFileStore := storemocks.FileInfoStore{}
|
||||
mockFileStore.On("GetUptoNSizeFileTime", mock.Anything).Return(int64(1), nil)
|
||||
mockSystemStore := storemocks.SystemStore{}
|
||||
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
|
||||
mockStore.On("FileInfo").Return(&mockFileStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockFileStore := storemocks.FileInfoStore{}
|
||||
mockFileStore.On("GetUptoNSizeFileTime", mock.Anything).Return(int64(1), nil)
|
||||
mockSystemStore := storemocks.SystemStore{}
|
||||
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
|
||||
mockStore.On("FileInfo").Return(&mockFileStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
|
||||
err := th.App.ComputeLastAccessibleFileTime()
|
||||
require.NoError(t, err)
|
||||
err := th.App.ComputeLastAccessibleFileTime()
|
||||
require.NoError(t, err)
|
||||
|
||||
mockSystemStore.AssertCalled(t, "SaveOrUpdate", mock.Anything)
|
||||
mockSystemStore.AssertCalled(t, "SaveOrUpdate", mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("Removes the time, if cloud limit is not applicable", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := &eMocks.CloudInterface{}
|
||||
th.App.Srv().Cloud = cloud
|
||||
|
||||
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, nil)
|
||||
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockFileStore := storemocks.FileInfoStore{}
|
||||
mockFileStore.On("GetUptoNSizeFileTime", mock.Anything).Return(int64(1), nil)
|
||||
mockSystemStore := storemocks.SystemStore{}
|
||||
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessibleFileTime, Value: "10"}, nil)
|
||||
mockSystemStore.On("PermanentDeleteByName", mock.Anything).Return(nil, nil)
|
||||
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
|
||||
mockStore.On("FileInfo").Return(&mockFileStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
|
||||
err := th.App.ComputeLastAccessibleFileTime()
|
||||
require.NoError(t, err)
|
||||
|
||||
mockSystemStore.AssertNotCalled(t, "SaveOrUpdate", mock.Anything)
|
||||
mockSystemStore.AssertCalled(t, "PermanentDeleteByName", mock.Anything)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package imports
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
@@ -22,6 +23,14 @@ type LineImportData struct {
|
||||
DirectPost *DirectPostImportData `json:"direct_post,omitempty"`
|
||||
Emoji *EmojiImportData `json:"emoji,omitempty"`
|
||||
Version *int `json:"version,omitempty"`
|
||||
Info *VersionInfoImportData `json:"info,omitempty"`
|
||||
}
|
||||
|
||||
type VersionInfoImportData struct {
|
||||
Generator string `json:"generator"`
|
||||
Version string `json:"version"`
|
||||
Created string `json:"created"`
|
||||
Additional json.RawMessage `json:"additional,omitempty"`
|
||||
}
|
||||
|
||||
type TeamImportData struct {
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (a *App) checkIntegrationLimitsForConfigSave(oldConfig, newConfig *model.Config) *model.AppError {
|
||||
pluginIds := []string{}
|
||||
for pluginId, newState := range newConfig.PluginSettings.PluginStates {
|
||||
oldState, ok := oldConfig.PluginSettings.PluginStates[pluginId]
|
||||
if newState.Enable && !(ok && oldState.Enable) {
|
||||
pluginIds = append(pluginIds, pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(pluginIds) > 0 {
|
||||
return a.checkIfIntegrationsMeetFreemiumLimits(pluginIds)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ch *Channels) getInstalledIntegrations() ([]*model.InstalledIntegration, *model.AppError) {
|
||||
out := []*model.InstalledIntegration{}
|
||||
|
||||
pluginsEnvironment := ch.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("getInstalledIntegrations", "app.plugin.sync.read_local_folder.app_error", nil, "", 0).Wrap(err)
|
||||
}
|
||||
|
||||
pluginStates := ch.cfgSvc.Config().PluginSettings.PluginStates
|
||||
for _, p := range plugins {
|
||||
if _, ok := model.InstalledIntegrationsIgnoredPlugins[p.Manifest.Id]; !ok {
|
||||
enabled := false
|
||||
if state, ok := pluginStates[p.Manifest.Id]; ok {
|
||||
enabled = state.Enable
|
||||
}
|
||||
|
||||
integration := &model.InstalledIntegration{
|
||||
Type: "plugin",
|
||||
ID: p.Manifest.Id,
|
||||
Name: p.Manifest.Name,
|
||||
Version: p.Manifest.Version,
|
||||
Enabled: enabled,
|
||||
}
|
||||
|
||||
out = append(out, integration)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort result alphabetically, by display name.
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
||||
})
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *App) checkIfIntegrationsMeetFreemiumLimits(originalPluginIds []string) *model.AppError {
|
||||
if !a.License().IsCloud() {
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginIds := map[string]bool{}
|
||||
for _, pluginId := range originalPluginIds {
|
||||
if _, ok := model.InstalledIntegrationsIgnoredPlugins[pluginId]; !ok {
|
||||
pluginIds[pluginId] = true
|
||||
}
|
||||
}
|
||||
|
||||
limits, err := a.Cloud().GetCloudLimits("")
|
||||
if err != nil {
|
||||
a.Log().Error("Error fetching cloud limits for enabled integrations", mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if limits == nil || limits.Integrations == nil || limits.Integrations.Enabled == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
installed, appErr := a.ch.getInstalledIntegrations()
|
||||
if appErr != nil {
|
||||
a.Log().Error("Failed to get installed integrations to check cloud limit", mlog.Err(appErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
enableCount := len(pluginIds)
|
||||
for _, integration := range installed {
|
||||
if _, ok := pluginIds[integration.ID]; !ok && integration.Enabled {
|
||||
enableCount++
|
||||
}
|
||||
}
|
||||
|
||||
limit := *limits.Integrations.Enabled
|
||||
if enableCount > limit {
|
||||
return model.NewAppError("checkIfIntegrationMeetsFreemiumLimits", "app.install_integration.reached_max_limit.error", map[string]any{"NumIntegrations": limit}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetIntegrationsUsage(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
samplePluginCode := `
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&MyPlugin{})
|
||||
}
|
||||
`
|
||||
|
||||
setupMultiPluginAPITest(t,
|
||||
[]string{samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode, samplePluginCode}, []string{
|
||||
`{"id": "otherplugin", "name": "Other Plugin", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
|
||||
`{"id": "mattermost-autolink", "name": "Autolink", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
|
||||
`{"id": "playbooks", "name": "Playbooks", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
|
||||
`{"id": "focalboard", "name": "Mattermost Boards", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
|
||||
`{"id": "com.mattermost.calls", "name": "Calls", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
|
||||
`{"id": "com.mattermost.nps", "name": "User Satisfaction Surveys", "version": "1.2.0", "server": {"executable": "backend.exe"}}`,
|
||||
`{"id": "com.mattermost.apps", "server": {"executable": "backend.exe"}}`,
|
||||
}, []string{"otherplugin", "mattermost-autolink", "playbooks", "focalboard", "com.mattermost.calls", "com.mattermost.nps", "com.mattermost.apps"},
|
||||
true, th.App, th.Context)
|
||||
|
||||
integrations, appErr := th.App.ch.getInstalledIntegrations()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
expected := []*model.InstalledIntegration{
|
||||
{
|
||||
Type: "plugin",
|
||||
ID: "mattermost-autolink",
|
||||
Name: "Autolink",
|
||||
Version: "1.2.0",
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
Type: "plugin",
|
||||
ID: "otherplugin",
|
||||
Name: "Other Plugin",
|
||||
Version: "1.2.0",
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Equal(t, expected, integrations)
|
||||
|
||||
usage, appErr := th.App.GetIntegrationsUsage()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// 2 enabled integrations
|
||||
expectedUsage := &model.IntegrationsUsage{
|
||||
Enabled: 2,
|
||||
}
|
||||
require.Equal(t, expectedUsage, usage)
|
||||
}
|
||||
33
app/login.go
33
app/login.go
@@ -157,17 +157,15 @@ func (a *App) GetUserForLogin(id, loginId string) (*model.User, *model.AppError)
|
||||
}
|
||||
|
||||
func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile, isOAuthUser, isSaml bool) *model.AppError {
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var rejectionReason string
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
rejectionReason = hooks.UserWillLogIn(pluginContext, user)
|
||||
return rejectionReason == ""
|
||||
}, plugin.UserWillLogInID)
|
||||
var rejectionReason string
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
rejectionReason = hooks.UserWillLogIn(pluginContext, user)
|
||||
return rejectionReason == ""
|
||||
}, plugin.UserWillLogInID)
|
||||
|
||||
if rejectionReason != "" {
|
||||
return model.NewAppError("DoLogin", "Login rejected by plugin: "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
}
|
||||
if rejectionReason != "" {
|
||||
return model.NewAppError("DoLogin", "Login rejected by plugin: "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceID, IsOAuth: false, Props: map[string]string{
|
||||
@@ -226,15 +224,12 @@ func (a *App) DoLogin(c *request.Context, w http.ResponseWriter, r *http.Request
|
||||
})
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLoggedIn(pluginContext, user)
|
||||
return true
|
||||
}, plugin.UserHasLoggedInID)
|
||||
})
|
||||
}
|
||||
a.Srv().Go(func() {
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLoggedIn(pluginContext, user)
|
||||
return true
|
||||
}, plugin.UserHasLoggedInID)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -305,7 +305,8 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
|
||||
mentionedUsersList = append(mentionedUsersList, id)
|
||||
}
|
||||
|
||||
nErr := a.Srv().Store().Channel().IncrementMentionCount(post.ChannelId, mentionedUsersList, post.RootId == "")
|
||||
nErr := a.Srv().Store().Channel().IncrementMentionCount(post.ChannelId, mentionedUsersList, post.RootId == "", post.IsUrgent())
|
||||
|
||||
if nErr != nil {
|
||||
mlog.Warn(
|
||||
"Failed to update mention count",
|
||||
@@ -596,7 +597,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
|
||||
}
|
||||
threadMembership = tm
|
||||
}
|
||||
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true)
|
||||
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot get thread %q for user %q", post.RootId, uid)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
email "github.com/mattermost/mattermost-server/v6/app/email"
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
@@ -201,18 +202,6 @@ func truncateUserNames(name string, i int) string {
|
||||
return name
|
||||
}
|
||||
|
||||
type FieldRow struct {
|
||||
Cells []*model.SlackAttachmentField
|
||||
}
|
||||
|
||||
type EmailMessageAttachment struct {
|
||||
model.SlackAttachment
|
||||
|
||||
Pretext template.HTML
|
||||
Text template.HTML
|
||||
FieldRows []FieldRow
|
||||
}
|
||||
|
||||
type postData struct {
|
||||
SenderName string
|
||||
ChannelName string
|
||||
@@ -223,7 +212,7 @@ type postData struct {
|
||||
Time string
|
||||
ShowChannelIcon bool
|
||||
OtherChannelMembersCount int
|
||||
MessageAttachments []*EmailMessageAttachment
|
||||
MessageAttachments []*email.EmailMessageAttachment
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +247,7 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos
|
||||
}
|
||||
pData.Message = template.HTML(normalizedPostMessage)
|
||||
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
|
||||
pData.MessageAttachments = a.processMessageAttachments(post)
|
||||
pData.MessageAttachments = email.ProcessMessageAttachments(post)
|
||||
}
|
||||
|
||||
data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale)
|
||||
@@ -320,81 +309,6 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos
|
||||
return a.Srv().TemplatesContainer().RenderToString("messages_notification", data)
|
||||
}
|
||||
|
||||
func (a *App) processMessageAttachments(post *model.Post) []*EmailMessageAttachment {
|
||||
emailMessageAttachments := []*EmailMessageAttachment{}
|
||||
|
||||
for _, messageAttachment := range post.Attachments() {
|
||||
emailMessageAttachment := &EmailMessageAttachment{
|
||||
SlackAttachment: *messageAttachment,
|
||||
Pretext: a.prepareTextForEmail(messageAttachment.Pretext),
|
||||
Text: a.prepareTextForEmail(messageAttachment.Text),
|
||||
}
|
||||
|
||||
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
|
||||
if err != nil {
|
||||
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
stripedTitle = ""
|
||||
}
|
||||
|
||||
emailMessageAttachment.Title = stripedTitle
|
||||
|
||||
shortFieldRow := FieldRow{}
|
||||
|
||||
for i := range messageAttachment.Fields {
|
||||
// Create a new instance to avoid altering the original pointer reference
|
||||
// We update field value to parse markdown.
|
||||
// If we do that on the original pointer, the rendered text in mattermost
|
||||
// becomes invalid as its no longer a markdown string, but rather an HTML string.
|
||||
field := &model.SlackAttachmentField{
|
||||
Title: messageAttachment.Fields[i].Title,
|
||||
Value: messageAttachment.Fields[i].Value,
|
||||
Short: messageAttachment.Fields[i].Short,
|
||||
}
|
||||
|
||||
if stringValue, ok := field.Value.(string); ok {
|
||||
field.Value = a.prepareTextForEmail(stringValue)
|
||||
}
|
||||
|
||||
if !field.Short {
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
|
||||
} else {
|
||||
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
|
||||
|
||||
if len(shortFieldRow.Cells) == 2 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect any leftover short fields
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
|
||||
}
|
||||
|
||||
return emailMessageAttachments
|
||||
}
|
||||
|
||||
func (a *App) prepareTextForEmail(text string) template.HTML {
|
||||
escapedText := html.EscapeString(text)
|
||||
markdownText, err := utils.MarkdownToHTML(escapedText)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
|
||||
return template.HTML(text)
|
||||
}
|
||||
|
||||
return template.HTML(markdownText)
|
||||
}
|
||||
|
||||
type formattedPostTime struct {
|
||||
Time time.Time
|
||||
Year string
|
||||
|
||||
@@ -53,7 +53,7 @@ func (a *App) CompleteOnboarding(c *request.Context, request *model.CompleteOnbo
|
||||
return
|
||||
}
|
||||
|
||||
hooks, err := pluginsEnvironment.HooksForPlugin(id)
|
||||
hooks, err := a.ch.HooksForPluginOrProduct(id)
|
||||
if err != nil {
|
||||
mlog.Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err))
|
||||
return
|
||||
|
||||
@@ -1200,28 +1200,6 @@ func (a *OpenTracingAppLayer) CheckForClientSideCert(r *http.Request) (string, s
|
||||
return resultVar0, resultVar1, resultVar2
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CheckFreemiumLimitsForConfigSave(oldConfig *model.Config, newConfig *model.Config) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckFreemiumLimitsForConfigSave")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.CheckFreemiumLimitsForConfigSave(oldConfig, newConfig)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CheckIntegrity() <-chan model.IntegrityCheckResult {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CheckIntegrity")
|
||||
@@ -2029,6 +2007,28 @@ func (a *OpenTracingAppLayer) CreateDefaultMemberships(c *request.Context, param
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CreateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateDraft")
|
||||
|
||||
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.CreateDraft(c, draft, connectionID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CreateEmoji(c request.CTX, sessionUserId string, emoji *model.Emoji, multiPartImageData *multipart.Form) (*model.Emoji, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateEmoji")
|
||||
@@ -2860,6 +2860,28 @@ func (a *OpenTracingAppLayer) DefaultChannelNames(c request.CTX) []string {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteAcknowledgementForPost(c *request.Context, postID string, userID string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAcknowledgementForPost")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.DeleteAcknowledgementForPost(c, postID, userID)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteAllExpiredPluginKeys() *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAllExpiredPluginKeys")
|
||||
@@ -2992,6 +3014,28 @@ func (a *OpenTracingAppLayer) DeleteCommand(commandID string) *model.AppError {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteDraft(userID string, channelID string, rootID string, connectionID string) (*model.Draft, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteDraft")
|
||||
|
||||
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.DeleteDraft(userID, channelID, rootID, connectionID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DeleteEmoji(c request.CTX, emoji *model.Emoji) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteEmoji")
|
||||
@@ -4433,6 +4477,50 @@ func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPost")
|
||||
|
||||
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.GetAcknowledgementsForPost(postID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPostList")
|
||||
|
||||
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.GetAcknowledgementsForPostList(postList)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetActivePluginManifests")
|
||||
@@ -5804,6 +5892,50 @@ func (a *OpenTracingAppLayer) GetDeletedChannels(c request.CTX, teamID string, o
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetDraft(userID string, channelID string, rootID string) (*model.Draft, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDraft")
|
||||
|
||||
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.GetDraft(userID, channelID, rootID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetDraftsForUser(userID string, teamID string) ([]*model.Draft, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetDraftsForUser")
|
||||
|
||||
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.GetDraftsForUser(userID, teamID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetEmoji(c request.CTX, emojiId string) (*model.Emoji, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetEmoji")
|
||||
@@ -6630,28 +6762,6 @@ func (a *OpenTracingAppLayer) GetIncomingWebhooksPageByUser(userID string, page
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetIntegrationsUsage")
|
||||
|
||||
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.GetIntegrationsUsage()
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetJob(id string) (*model.Job, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetJob")
|
||||
@@ -7842,6 +7952,28 @@ func (a *OpenTracingAppLayer) GetPostIfAuthorized(c request.CTX, postID string,
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostInfo")
|
||||
|
||||
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.GetPostInfo(c, postID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostThread")
|
||||
@@ -8162,6 +8294,50 @@ func (a *OpenTracingAppLayer) GetPrevPostIdFromPostList(postList *model.PostList
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPost")
|
||||
|
||||
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.GetPriorityForPost(postId)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPriorityForPostList")
|
||||
|
||||
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.GetPriorityForPostList(list)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetPrivateChannelsForTeam(c request.CTX, teamID string, offset int, limit int) (model.ChannelList, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPrivateChannelsForTeam")
|
||||
@@ -11060,6 +11236,50 @@ func (a *OpenTracingAppLayer) GetWarnMetricsStatus() (map[string]*model.WarnMetr
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplateCategories")
|
||||
|
||||
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.GetWorkTemplateCategories(t)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWorkTemplates")
|
||||
|
||||
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.GetWorkTemplates(category, featureFlags, t)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
|
||||
@@ -11721,23 +11941,6 @@ func (a *OpenTracingAppLayer) IsPhase2MigrationCompleted() *model.AppError {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) IsUserAway(lastActivityAt int64) bool {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUserAway")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.IsUserAway(lastActivityAt)
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) IsUserSignUpAllowed() *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUserSignUpAllowed")
|
||||
@@ -13067,7 +13270,7 @@ func (a *OpenTracingAppLayer) PostWithProxyRemovedFromImageURLs(post *model.Post
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool) *model.Post {
|
||||
func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClient")
|
||||
|
||||
@@ -13079,12 +13282,12 @@ func (a *OpenTracingAppLayer) PreparePostForClient(c request.CTX, originalPost *
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.PreparePostForClient(c, originalPost, isNewPost, isEditPost)
|
||||
resultVar0 := a.app.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority)
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool) *model.Post {
|
||||
func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost bool, isEditPost bool, includePriority bool) *model.Post {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PreparePostForClientWithEmbedsAndImages")
|
||||
|
||||
@@ -13096,7 +13299,7 @@ func (a *OpenTracingAppLayer) PreparePostForClientWithEmbedsAndImages(c request.
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.PreparePostForClientWithEmbedsAndImages(c, originalPost, isNewPost, isEditPost)
|
||||
resultVar0 := a.app.PreparePostForClientWithEmbedsAndImages(c, originalPost, isNewPost, isEditPost, includePriority)
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
@@ -14312,6 +14515,28 @@ func (a *OpenTracingAppLayer) SanitizeTeams(session model.Session, teams []*mode
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SaveAcknowledgementForPost(c *request.Context, postID string, userID string) (*model.PostAcknowledgement, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAcknowledgementForPost")
|
||||
|
||||
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.SaveAcknowledgementForPost(c, postID, userID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotification")
|
||||
@@ -15265,6 +15490,28 @@ func (a *OpenTracingAppLayer) SendPaymentFailedEmail(failedPayment *model.Failed
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendSubscriptionHistoryEvent")
|
||||
|
||||
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.SendSubscriptionHistoryEvent(userID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendTestPushNotification")
|
||||
@@ -15282,7 +15529,7 @@ func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail() *model.AppError {
|
||||
func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail(isYearly bool) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendUpgradeConfirmationEmail")
|
||||
|
||||
@@ -15294,7 +15541,7 @@ func (a *OpenTracingAppLayer) SendUpgradeConfirmationEmail() *model.AppError {
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.SendUpgradeConfirmationEmail()
|
||||
resultVar0 := a.app.SendUpgradeConfirmationEmail(isYearly)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
@@ -16880,6 +17127,28 @@ func (a *OpenTracingAppLayer) UpdateDNDStatusOfUsers() {
|
||||
a.app.UpdateDNDStatusOfUsers()
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateDraft")
|
||||
|
||||
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.UpdateDraft(c, draft, connectionID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateEphemeralPost")
|
||||
@@ -17029,21 +17298,6 @@ func (a *OpenTracingAppLayer) UpdateIncomingWebhook(oldHook *model.IncomingWebho
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateLastActivityAtIfNeeded(session model.Session) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateLastActivityAtIfNeeded")
|
||||
|
||||
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.UpdateLastActivityAtIfNeeded(session)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateMfa(c request.CTX, activate bool, userID string, token string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateMfa")
|
||||
@@ -17969,6 +18223,28 @@ func (a *OpenTracingAppLayer) UploadFileX(c *request.Context, channelID string,
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpsertDraft(c *request.Context, draft *model.Draft, connectionID string) (*model.Draft, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertDraft")
|
||||
|
||||
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.UpsertDraft(c, draft, connectionID)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpsertGroupMember")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
"github.com/mattermost/mattermost-server/v6/store/sqlstore"
|
||||
)
|
||||
|
||||
type permissionTransformation struct {
|
||||
@@ -74,18 +75,73 @@ const (
|
||||
PermissionManageRemoteClusters = "manage_remote_clusters" // deprecated; use `manage_secure_connections`
|
||||
)
|
||||
|
||||
func isRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
|
||||
// Deprecated: This function should only be used if a case arises where team and/or channel scheme roles do not need to be migrated.
|
||||
// Otherwise, use isRole.
|
||||
func isExactRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
|
||||
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
|
||||
return role.Name == roleName
|
||||
}
|
||||
}
|
||||
|
||||
func isNotRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
|
||||
// isRole returns true if roleName matches a role's name field or if the a team
|
||||
// or channel scheme role matches a "common name". A common name is one of the following role
|
||||
// that is common among the system scheme and the team and/or channel schemes:
|
||||
//
|
||||
// TeamAdmin,
|
||||
// TeamUser,
|
||||
// TeamGuest,
|
||||
// ChannelAdmin,
|
||||
// ChannelUser,
|
||||
// ChannelGuest,
|
||||
// PlaybookAdmin,
|
||||
// PlaybookMember,
|
||||
// RunAdmin,
|
||||
// RunMember
|
||||
func isRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
|
||||
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
|
||||
if role.Name == roleName {
|
||||
return true
|
||||
}
|
||||
return isSchemeRoleAssociatedToCommonName(roleName, role)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: use isNotRole instead.
|
||||
func isNotExactRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
|
||||
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
|
||||
return role.Name != roleName
|
||||
}
|
||||
}
|
||||
|
||||
func isNotRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
|
||||
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
|
||||
return role.Name != roleName && !isSchemeRoleAssociatedToCommonName(roleName, role)
|
||||
}
|
||||
}
|
||||
|
||||
func isSchemeRoleAssociatedToCommonName(roleName string, role *model.Role) bool {
|
||||
roleIDToSchemeRoleDisplayName := map[string]string{
|
||||
model.TeamAdminRoleId: sqlstore.SchemeRoleDisplayNameTeamAdmin,
|
||||
model.TeamUserRoleId: sqlstore.SchemeRoleDisplayNameTeamUser,
|
||||
model.TeamGuestRoleId: sqlstore.SchemeRoleDisplayNameTeamGuest,
|
||||
|
||||
model.ChannelAdminRoleId: sqlstore.SchemeRoleDisplayNameChannelAdmin,
|
||||
model.ChannelUserRoleId: sqlstore.SchemeRoleDisplayNameChannelUser,
|
||||
model.ChannelGuestRoleId: sqlstore.SchemeRoleDisplayNameChannelGuest,
|
||||
|
||||
model.PlaybookAdminRoleId: sqlstore.SchemeRoleDisplayNamePlaybookAdmin,
|
||||
model.PlaybookMemberRoleId: sqlstore.SchemeRoleDisplayNamePlaybookMember,
|
||||
|
||||
model.RunAdminRoleId: sqlstore.SchemeRoleDisplayNameRunAdmin,
|
||||
model.RunMemberRoleId: sqlstore.SchemeRoleDisplayNameRunMember,
|
||||
}
|
||||
displayName, ok := roleIDToSchemeRoleDisplayName[roleName]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(role.DisplayName, displayName)
|
||||
}
|
||||
|
||||
func isNotSchemeRole(roleName string) func(*model.Role, map[string]map[string]bool) bool {
|
||||
return func(role *model.Role, permissionsMap map[string]map[string]bool) bool {
|
||||
return !strings.Contains(role.DisplayName, roleName)
|
||||
@@ -222,12 +278,12 @@ func (a *App) getWebhooksPermissionsSplitMigration() (permissionsMap, error) {
|
||||
func (a *App) getListJoinPublicPrivateTeamsPermissionsMigration() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{PermissionListPrivateTeams, PermissionJoinPrivateTeams},
|
||||
Remove: []string{},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemUserRoleId),
|
||||
On: isExactRole(model.SystemUserRoleId),
|
||||
Add: []string{PermissionListPublicTeams, PermissionJoinPublicTeams},
|
||||
Remove: []string{},
|
||||
},
|
||||
@@ -246,7 +302,7 @@ func (a *App) removePermanentDeleteUserMigration() (permissionsMap, error) {
|
||||
func (a *App) getAddBotPermissionsMigration() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{PermissionCreateBot, PermissionReadBots, PermissionReadOthersBots, PermissionManageBots, PermissionManageOthersBots},
|
||||
Remove: []string{},
|
||||
},
|
||||
@@ -256,19 +312,19 @@ func (a *App) getAddBotPermissionsMigration() (permissionsMap, error) {
|
||||
func (a *App) applyChannelManageDeleteToChannelUser() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePrivateChannelProperties))),
|
||||
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePrivateChannelProperties))),
|
||||
Add: []string{PermissionManagePrivateChannelProperties},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePrivateChannel))),
|
||||
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePrivateChannel))),
|
||||
Add: []string{PermissionDeletePrivateChannel},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePublicChannelProperties))),
|
||||
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionManagePublicChannelProperties))),
|
||||
Add: []string{PermissionManagePublicChannelProperties},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePublicChannel))),
|
||||
On: permissionAnd(isExactRole(model.ChannelUserRoleId), onOtherRole(model.TeamUserRoleId, permissionExists(PermissionDeletePublicChannel))),
|
||||
Add: []string{PermissionDeletePublicChannel},
|
||||
},
|
||||
}, nil
|
||||
@@ -277,19 +333,19 @@ func (a *App) applyChannelManageDeleteToChannelUser() (permissionsMap, error) {
|
||||
func (a *App) removeChannelManageDeleteFromTeamUser() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.TeamUserRoleId), permissionExists(PermissionManagePrivateChannelProperties)),
|
||||
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionManagePrivateChannelProperties)),
|
||||
Remove: []string{PermissionManagePrivateChannelProperties},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.TeamUserRoleId), permissionExists(PermissionDeletePrivateChannel)),
|
||||
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionDeletePrivateChannel)),
|
||||
Remove: []string{model.PermissionDeletePrivateChannel.Id},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.TeamUserRoleId), permissionExists(PermissionManagePublicChannelProperties)),
|
||||
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionManagePublicChannelProperties)),
|
||||
Remove: []string{PermissionManagePublicChannelProperties},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: permissionAnd(isRole(model.TeamUserRoleId), permissionExists(PermissionDeletePublicChannel)),
|
||||
On: permissionAnd(isExactRole(model.TeamUserRoleId), permissionExists(PermissionDeletePublicChannel)),
|
||||
Remove: []string{PermissionDeletePublicChannel},
|
||||
},
|
||||
}, nil
|
||||
@@ -298,11 +354,11 @@ func (a *App) removeChannelManageDeleteFromTeamUser() (permissionsMap, error) {
|
||||
func (a *App) getViewMembersPermissionMigration() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemUserRoleId),
|
||||
On: isExactRole(model.SystemUserRoleId),
|
||||
Add: []string{PermissionViewMembers},
|
||||
},
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{PermissionViewMembers},
|
||||
},
|
||||
}, nil
|
||||
@@ -311,7 +367,7 @@ func (a *App) getViewMembersPermissionMigration() (permissionsMap, error) {
|
||||
func (a *App) getAddManageGuestsPermissionsMigration() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{PermissionPromoteGuest, PermissionDemoteToGuest, PermissionInviteGuest},
|
||||
},
|
||||
}, nil
|
||||
@@ -342,7 +398,7 @@ func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) {
|
||||
// add each moderated permission to the channel admin if channel user or guest has the permission
|
||||
trans := permissionTransformation{
|
||||
On: permissionAnd(
|
||||
isRole(channelAdminID),
|
||||
isExactRole(channelAdminID),
|
||||
permissionOr(
|
||||
onOtherRole(channelUserID, permissionExists(perm)),
|
||||
onOtherRole(channelGuestID, permissionExists(perm)),
|
||||
@@ -355,7 +411,7 @@ func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) {
|
||||
// add each moderated permission to the team admin if channel admin, user, or guest has the permission
|
||||
trans = permissionTransformation{
|
||||
On: permissionAnd(
|
||||
isRole(teamAdminID),
|
||||
isExactRole(teamAdminID),
|
||||
permissionOr(
|
||||
onOtherRole(channelAdminID, permissionExists(perm)),
|
||||
onOtherRole(channelUserID, permissionExists(perm)),
|
||||
@@ -373,14 +429,14 @@ func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) {
|
||||
for _, ts := range allTeamSchemes {
|
||||
// ensure all team scheme channel admins have create_post because it's not exposed via the UI
|
||||
trans := permissionTransformation{
|
||||
On: isRole(ts.DefaultChannelAdminRole),
|
||||
On: isExactRole(ts.DefaultChannelAdminRole),
|
||||
Add: []string{PermissionCreatePost},
|
||||
}
|
||||
transformations = append(transformations, trans)
|
||||
|
||||
// ensure all team scheme team admins have create_post because it's not exposed via the UI
|
||||
trans = permissionTransformation{
|
||||
On: isRole(ts.DefaultTeamAdminRole),
|
||||
On: isExactRole(ts.DefaultTeamAdminRole),
|
||||
Add: []string{PermissionCreatePost},
|
||||
}
|
||||
transformations = append(transformations, trans)
|
||||
@@ -396,13 +452,13 @@ func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) {
|
||||
|
||||
// ensure team admins have create_post
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: isRole(model.TeamAdminRoleId),
|
||||
On: isExactRole(model.TeamAdminRoleId),
|
||||
Add: []string{PermissionCreatePost},
|
||||
})
|
||||
|
||||
// ensure channel admins have create_post
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: isRole(model.ChannelAdminRoleId),
|
||||
On: isExactRole(model.ChannelAdminRoleId),
|
||||
Add: []string{PermissionCreatePost},
|
||||
})
|
||||
|
||||
@@ -416,7 +472,7 @@ func (a *App) channelModerationPermissionsMigration() (permissionsMap, error) {
|
||||
|
||||
// ensure system admin has all of the moderated permissions
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: append(moderatedPermissionsMinusCreatePost, PermissionCreatePost),
|
||||
})
|
||||
|
||||
@@ -433,8 +489,8 @@ func (a *App) getAddUseGroupMentionsPermissionMigration() (permissionsMap, error
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: permissionAnd(
|
||||
isNotRole(model.ChannelGuestRoleId),
|
||||
isNotSchemeRole("Channel Guest Role for Scheme"),
|
||||
isNotExactRole(model.ChannelGuestRoleId),
|
||||
isNotSchemeRole(sqlstore.SchemeRoleDisplayNameChannelGuest),
|
||||
permissionOr(permissionExists(PermissionCreatePost), permissionExists(PermissionCreatePost_PUBLIC)),
|
||||
),
|
||||
Add: []string{PermissionUseGroupMentions},
|
||||
@@ -453,7 +509,7 @@ func (a *App) getAddSystemConsolePermissionsMigration() (permissionsMap, error)
|
||||
// add the new permissions to system admin
|
||||
transformations = append(transformations,
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: permissionsToAdd,
|
||||
})
|
||||
|
||||
@@ -502,7 +558,7 @@ func (a *App) getAddConvertChannelPermissionsMigration() (permissionsMap, error)
|
||||
func (a *App) getSystemRolesPermissionsMigration() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{model.PermissionSysconsoleReadUserManagementSystemRoles.Id, model.PermissionSysconsoleWriteUserManagementSystemRoles.Id},
|
||||
},
|
||||
}, nil
|
||||
@@ -511,7 +567,7 @@ func (a *App) getSystemRolesPermissionsMigration() (permissionsMap, error) {
|
||||
func (a *App) getAddManageSharedChannelsPermissionsMigration() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{PermissionManageSharedChannels},
|
||||
},
|
||||
}, nil
|
||||
@@ -520,7 +576,7 @@ func (a *App) getAddManageSharedChannelsPermissionsMigration() (permissionsMap,
|
||||
func (a *App) getBillingPermissionsMigration() (permissionsMap, error) {
|
||||
return permissionsMap{
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{model.PermissionSysconsoleReadBilling.Id, model.PermissionSysconsoleWriteBilling.Id},
|
||||
},
|
||||
}, nil
|
||||
@@ -532,14 +588,14 @@ func (a *App) getAddManageSecureConnectionsPermissionsMigration() (permissionsMa
|
||||
// add the new permission to system admin
|
||||
transformations = append(transformations,
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{PermissionManageSecureConnections},
|
||||
})
|
||||
|
||||
// remote the deprecated permission from system admin
|
||||
transformations = append(transformations,
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Remove: []string{PermissionManageRemoteClusters},
|
||||
})
|
||||
|
||||
@@ -555,7 +611,7 @@ func (a *App) getAddDownloadComplianceExportResult() (permissionsMap, error) {
|
||||
// add the new permissions to system admin
|
||||
transformations = append(transformations,
|
||||
permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{model.PermissionDownloadComplianceExportResult.Id},
|
||||
})
|
||||
|
||||
@@ -926,12 +982,12 @@ func (a *App) getAddCustomUserGroupsPermissions() (permissionsMap, error) {
|
||||
}
|
||||
|
||||
t = append(t, permissionTransformation{
|
||||
On: isRole(model.SystemUserRoleId),
|
||||
On: isExactRole(model.SystemUserRoleId),
|
||||
Add: customGroupPermissions,
|
||||
})
|
||||
|
||||
t = append(t, permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: customGroupPermissions,
|
||||
})
|
||||
|
||||
@@ -953,7 +1009,7 @@ func (a *App) getAddPlaybooksPermissions() (permissionsMap, error) {
|
||||
})
|
||||
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: isRole(model.SystemAdminRoleId),
|
||||
On: isExactRole(model.SystemAdminRoleId),
|
||||
Add: []string{
|
||||
model.PermissionPublicPlaybookManageProperties.Id,
|
||||
model.PermissionPublicPlaybookManageMembers.Id,
|
||||
@@ -978,9 +1034,9 @@ func (a *App) getPlaybooksPermissionsAddManageRoles() (permissionsMap, error) {
|
||||
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: permissionOr(
|
||||
isRole(model.PlaybookAdminRoleId),
|
||||
isRole(model.TeamAdminRoleId),
|
||||
isRole(model.SystemAdminRoleId),
|
||||
isExactRole(model.PlaybookAdminRoleId),
|
||||
isExactRole(model.TeamAdminRoleId),
|
||||
isExactRole(model.SystemAdminRoleId),
|
||||
),
|
||||
Add: []string{
|
||||
model.PermissionPublicPlaybookManageRoles.Id,
|
||||
@@ -999,13 +1055,13 @@ func (a *App) getProductsBoardsPermissions() (permissionsMap, error) {
|
||||
|
||||
// Give the new subsection READ permissions to any user with SYSTEM_MANAGER
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: permissionOr(isRole(model.SystemManagerRoleId)),
|
||||
On: permissionOr(isExactRole(model.SystemManagerRoleId)),
|
||||
Add: permissionsProductsRead,
|
||||
})
|
||||
|
||||
// Give the new subsection WRITE permissions to any user with SYSTEM_ADMIN
|
||||
transformations = append(transformations, permissionTransformation{
|
||||
On: permissionOr(isRole(model.SystemAdminRoleId)),
|
||||
On: permissionOr(isExactRole(model.SystemAdminRoleId)),
|
||||
Add: permissionsProductsWrite,
|
||||
})
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/store/sqlstore"
|
||||
)
|
||||
|
||||
func TestApplyPermissionsMap(t *testing.T) {
|
||||
@@ -137,7 +138,7 @@ func TestApplyPermissionsMap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
permissionsMap{permissionTransformation{
|
||||
On: isRole("system_admin"),
|
||||
On: isExactRole("system_admin"),
|
||||
Add: []string{"test4"},
|
||||
}},
|
||||
[]string{"test1", "test2", "test3", "test4"},
|
||||
@@ -152,7 +153,7 @@ func TestApplyPermissionsMap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
permissionsMap{permissionTransformation{
|
||||
On: isRole("system_user"),
|
||||
On: isExactRole("system_user"),
|
||||
Add: []string{"test4"},
|
||||
}},
|
||||
[]string{"test1", "test2", "test3"},
|
||||
@@ -203,3 +204,74 @@ func TestApplyPermissionsMap(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPermissionsMapToSchemeRole(t *testing.T) {
|
||||
schemeRoleName := model.NewId()
|
||||
tt := []struct {
|
||||
Name string
|
||||
RoleMap map[string]map[string]bool
|
||||
TranslationMap permissionsMap
|
||||
ExpectedResult []string
|
||||
}{
|
||||
{
|
||||
"Adds a permission to a scheme role with a matching common name",
|
||||
map[string]map[string]bool{
|
||||
schemeRoleName: {
|
||||
"test1": true,
|
||||
},
|
||||
},
|
||||
permissionsMap{permissionTransformation{
|
||||
On: isRole(model.TeamAdminRoleId),
|
||||
Add: []string{"test2"},
|
||||
}},
|
||||
[]string{"test1", "test2"},
|
||||
},
|
||||
{
|
||||
"Doesn't add a permission to a scheme role with a different common name",
|
||||
map[string]map[string]bool{
|
||||
schemeRoleName: {
|
||||
"test1": true,
|
||||
},
|
||||
},
|
||||
permissionsMap{permissionTransformation{
|
||||
On: isRole(model.ChannelAdminRoleId),
|
||||
Add: []string{"test2"},
|
||||
}},
|
||||
[]string{"test1"},
|
||||
},
|
||||
{
|
||||
"Doesn't add a permission to a role with a the same exact name",
|
||||
map[string]map[string]bool{
|
||||
schemeRoleName: {
|
||||
"test1": true,
|
||||
},
|
||||
},
|
||||
permissionsMap{permissionTransformation{
|
||||
On: isNotRole(schemeRoleName),
|
||||
Add: []string{"test2"},
|
||||
}},
|
||||
[]string{"test1"},
|
||||
},
|
||||
{
|
||||
"Doesn't add a permission to a role with a different exact name but the same common name",
|
||||
map[string]map[string]bool{
|
||||
schemeRoleName: {
|
||||
"test1": true,
|
||||
},
|
||||
},
|
||||
permissionsMap{permissionTransformation{
|
||||
On: isNotRole(model.TeamAdminRoleId),
|
||||
Add: []string{"test2"},
|
||||
}},
|
||||
[]string{"test1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
result := applyPermissionsMap(&model.Role{Name: schemeRoleName, DisplayName: sqlstore.SchemeRoleDisplayNameTeamAdmin}, tc.RoleMap, tc.TranslationMap)
|
||||
sort.Strings(result)
|
||||
assert.Equal(t, tc.ExpectedResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,6 @@ func (ps *PlatformService) SaveConfig(newCfg *model.Config, sendConfigChangeClus
|
||||
return nil, nil, model.NewAppError("saveConfig", "app.save_config.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if ps.startMetrics && *ps.Config().MetricsSettings.Enable {
|
||||
ps.RestartMetrics()
|
||||
} else {
|
||||
ps.ShutdownMetrics()
|
||||
}
|
||||
|
||||
if ps.clusterIFace != nil {
|
||||
err := ps.clusterIFace.ConfigChanged(ps.configStore.RemoveEnvironmentOverrides(oldCfg),
|
||||
ps.configStore.RemoveEnvironmentOverrides(newCfg), sendConfigChangeClusterMessage)
|
||||
|
||||
@@ -70,4 +70,31 @@ func TestConfigSave(t *testing.T) {
|
||||
updatedCfg := th.Service.Config()
|
||||
assert.Equal(t, "http://newhost.me", *updatedCfg.ServiceSettings.SiteURL)
|
||||
})
|
||||
|
||||
t.Run("do not restart the metrics server on a different type of config change", func(t *testing.T) {
|
||||
th := Setup(t, StartMetrics())
|
||||
defer th.TearDown()
|
||||
|
||||
metricsMock := &mocks.MetricsInterface{}
|
||||
metricsMock.On("IncrementWebsocketEvent", mock.AnythingOfType("string")).Return()
|
||||
metricsMock.On("IncrementWebSocketBroadcastBufferSize", mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
||||
metricsMock.On("DecrementWebSocketBroadcastBufferSize", mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return()
|
||||
metricsMock.On("Register").Return()
|
||||
th.Service.metricsIFace = metricsMock
|
||||
|
||||
// Change a random config setting
|
||||
cfg := th.Service.Config().Clone()
|
||||
cfg.ThemeSettings.EnableThemeSelection = model.NewBool(!*cfg.ThemeSettings.EnableThemeSelection)
|
||||
th.Service.SaveConfig(cfg, false)
|
||||
metricsMock.AssertNumberOfCalls(t, "Register", 0)
|
||||
|
||||
// Disable metrics
|
||||
cfg.MetricsSettings.Enable = model.NewBool(false)
|
||||
th.Service.SaveConfig(cfg, false)
|
||||
|
||||
// Change the metrics setting
|
||||
cfg.MetricsSettings.Enable = model.NewBool(true)
|
||||
th.Service.SaveConfig(cfg, false)
|
||||
metricsMock.AssertNumberOfCalls(t, "Register", 1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
|
||||
th.Service.SetLicense(nil)
|
||||
}
|
||||
|
||||
err = th.Service.Start(th.Suite)
|
||||
err = th.Service.Start()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -39,20 +39,6 @@ func (_m *SuiteIFace) GetSession(token string) (*model.Session, *model.AppError)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IsUserAway provides a mock function with given fields: lastActivityAt
|
||||
func (_m *SuiteIFace) IsUserAway(lastActivityAt int64) bool {
|
||||
ret := _m.Called(lastActivityAt)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(int64) bool); ok {
|
||||
r0 = rf(lastActivityAt)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RolesGrantPermission provides a mock function with given fields: roleNames, permissionId
|
||||
func (_m *SuiteIFace) RolesGrantPermission(roleNames []string, permissionId string) bool {
|
||||
ret := _m.Called(roleNames, permissionId)
|
||||
@@ -67,31 +53,6 @@ func (_m *SuiteIFace) RolesGrantPermission(roleNames []string, permissionId stri
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetStatusAwayIfNeeded provides a mock function with given fields: userID, manual
|
||||
func (_m *SuiteIFace) SetStatusAwayIfNeeded(userID string, manual bool) {
|
||||
_m.Called(userID, manual)
|
||||
}
|
||||
|
||||
// SetStatusLastActivityAt provides a mock function with given fields: userID, activityAt
|
||||
func (_m *SuiteIFace) SetStatusLastActivityAt(userID string, activityAt int64) {
|
||||
_m.Called(userID, activityAt)
|
||||
}
|
||||
|
||||
// SetStatusOffline provides a mock function with given fields: userID, manual
|
||||
func (_m *SuiteIFace) SetStatusOffline(userID string, manual bool) {
|
||||
_m.Called(userID, manual)
|
||||
}
|
||||
|
||||
// SetStatusOnline provides a mock function with given fields: userID, manual
|
||||
func (_m *SuiteIFace) SetStatusOnline(userID string, manual bool) {
|
||||
_m.Called(userID, manual)
|
||||
}
|
||||
|
||||
// UpdateLastActivityAtIfNeeded provides a mock function with given fields: session
|
||||
func (_m *SuiteIFace) UpdateLastActivityAtIfNeeded(session model.Session) {
|
||||
_m.Called(session)
|
||||
}
|
||||
|
||||
// UserCanSeeOtherUser provides a mock function with given fields: userID, otherUserId
|
||||
func (_m *SuiteIFace) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
|
||||
ret := _m.Called(userID, otherUserId)
|
||||
|
||||
@@ -95,7 +95,12 @@ type PlatformService struct {
|
||||
additionalClusterHandlers map[model.ClusterEvent]einterfaces.ClusterMessageHandler
|
||||
sharedChannelService SharedChannelServiceIFace
|
||||
|
||||
pluginEnv *plugin.Environment
|
||||
pluginEnv HookRunner
|
||||
}
|
||||
|
||||
type HookRunner interface {
|
||||
RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int)
|
||||
GetPluginsEnvironment() *plugin.Environment
|
||||
}
|
||||
|
||||
// New creates a new PlatformService.
|
||||
@@ -263,6 +268,14 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
|
||||
if mErr := ps.resetMetrics(); mErr != nil {
|
||||
return nil, mErr
|
||||
}
|
||||
|
||||
ps.configStore.AddListener(func(oldCfg, newCfg *model.Config) {
|
||||
if *oldCfg.MetricsSettings.Enable != *newCfg.MetricsSettings.Enable || *oldCfg.MetricsSettings.ListenAddress != *newCfg.MetricsSettings.ListenAddress {
|
||||
if mErr := ps.resetMetrics(); mErr != nil {
|
||||
mlog.Warn("Failed to reset metrics", mlog.Err(mErr))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Step 9: Init AsymmetricSigningKey depends on step 6 (store)
|
||||
@@ -299,8 +312,8 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformService) Start(suite SuiteIFace) error {
|
||||
ps.hubStart(suite)
|
||||
func (ps *PlatformService) Start() error {
|
||||
ps.hubStart()
|
||||
|
||||
ps.configListenerId = ps.AddConfigListener(func(_, _ *model.Config) {
|
||||
ps.regenerateClientConfig()
|
||||
@@ -426,17 +439,17 @@ func (ps *PlatformService) SetSharedChannelService(s SharedChannelServiceIFace)
|
||||
ps.sharedChannelService = s
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetPluginsEnvironment(env *plugin.Environment) {
|
||||
ps.pluginEnv = env
|
||||
func (ps *PlatformService) SetPluginsEnvironment(runner HookRunner) {
|
||||
ps.pluginEnv = runner
|
||||
}
|
||||
|
||||
// GetPluginStatuses meant to be used by cluster implementation
|
||||
func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
if ps.pluginEnv == nil {
|
||||
if ps.pluginEnv == nil || ps.pluginEnv.GetPluginsEnvironment() == nil {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
pluginStatuses, err := ps.pluginEnv.Statuses()
|
||||
pluginStatuses, err := ps.pluginEnv.GetPluginsEnvironment().Statuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.get_statuses.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
@@ -105,10 +105,9 @@ func TestMetrics(t *testing.T) {
|
||||
|
||||
// there is no config listener for the metrics
|
||||
// we handle it on config save step
|
||||
th.Service.UpdateConfig(func(c *model.Config) {
|
||||
c.MetricsSettings.Enable = model.NewBool(true)
|
||||
})
|
||||
th.Service.SaveConfig(th.Service.Config(), false)
|
||||
cfg := th.Service.Config().Clone()
|
||||
cfg.MetricsSettings.Enable = model.NewBool(true)
|
||||
th.Service.SaveConfig(cfg, false)
|
||||
|
||||
require.NotNil(t, th.Service.metrics)
|
||||
metricsAddr := strings.Replace(th.Service.metrics.listenAddr, "[::]", "http://localhost", 1)
|
||||
@@ -117,17 +116,14 @@ func TestMetrics(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
th.Service.UpdateConfig(func(c *model.Config) {
|
||||
c.MetricsSettings.Enable = model.NewBool(false)
|
||||
})
|
||||
th.Service.SaveConfig(th.Service.Config(), false)
|
||||
cfg.MetricsSettings.Enable = model.NewBool(false)
|
||||
th.Service.SaveConfig(cfg, false)
|
||||
|
||||
_, err = http.Get(metricsAddr)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("ensure the metrics server is started with advanced metrics", func(t *testing.T) {
|
||||
t.Skip("MM-47635")
|
||||
th := Setup(t, StartMetrics())
|
||||
defer th.TearDown()
|
||||
|
||||
|
||||
@@ -212,3 +212,198 @@ func (ps *PlatformService) GetStatus(userID string) (*model.Status, *model.AppEr
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// SetStatusLastActivityAt sets the last activity at for a user on the local app server and updates
|
||||
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
|
||||
// while an 'away' device is still connected
|
||||
func (ps *PlatformService) SetStatusLastActivityAt(userID string, activityAt int64) {
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = ps.GetStatus(userID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
status.LastActivityAt = activityAt
|
||||
|
||||
ps.AddStatusCacheSkipClusterSend(status)
|
||||
ps.SetStatusAwayIfNeeded(userID, false)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) UpdateLastActivityAtIfNeeded(session model.Session) {
|
||||
now := model.GetMillis()
|
||||
|
||||
ps.UpdateWebConnUserActivity(session, now)
|
||||
|
||||
if now-session.LastActivityAt < model.SessionActivityTimeout {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ps.Store.Session().UpdateLastActivityAt(session.Id, now); err != nil {
|
||||
mlog.Warn("Failed to update LastActivityAt", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id), mlog.Err(err))
|
||||
}
|
||||
|
||||
session.LastActivityAt = now
|
||||
ps.AddSessionToCache(&session)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetStatusOnline(userID string, manual bool) {
|
||||
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
broadcast := false
|
||||
|
||||
var oldStatus string = model.StatusOffline
|
||||
var oldTime int64
|
||||
var oldManual bool
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
|
||||
if status, err = ps.GetStatus(userID); err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
|
||||
broadcast = true
|
||||
} else {
|
||||
if status.Manual && !manual {
|
||||
return // manually set status always overrides non-manual one
|
||||
}
|
||||
|
||||
if status.Status != model.StatusOnline {
|
||||
broadcast = true
|
||||
}
|
||||
|
||||
oldStatus = status.Status
|
||||
oldTime = status.LastActivityAt
|
||||
oldManual = status.Manual
|
||||
|
||||
status.Status = model.StatusOnline
|
||||
status.Manual = false // for "online" there's no manual setting
|
||||
status.LastActivityAt = model.GetMillis()
|
||||
}
|
||||
|
||||
ps.AddStatusCache(status)
|
||||
|
||||
// Only update the database if the status has changed, the status has been manually set,
|
||||
// or enough time has passed since the previous action
|
||||
if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.StatusMinUpdateTime {
|
||||
if broadcast {
|
||||
if err := ps.Store.Status().SaveOrUpdate(status); err != nil {
|
||||
mlog.Warn("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
|
||||
}
|
||||
} else {
|
||||
if err := ps.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt); err != nil {
|
||||
mlog.Error("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if broadcast {
|
||||
ps.BroadcastStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetStatusOffline(userID string, manual bool) {
|
||||
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := ps.GetStatus(userID)
|
||||
if err == nil && status.Manual && !manual {
|
||||
return // manually set status always overrides non-manual one
|
||||
}
|
||||
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
|
||||
|
||||
ps.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetStatusAwayIfNeeded(userID string, manual bool) {
|
||||
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := ps.GetStatus(userID)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
if !manual && status.Manual {
|
||||
return // manually set status always overrides non-manual one
|
||||
}
|
||||
|
||||
if !manual {
|
||||
if status.Status == model.StatusAway {
|
||||
return
|
||||
}
|
||||
|
||||
if !ps.isUserAway(status.LastActivityAt) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
status.Status = model.StatusAway
|
||||
status.Manual = manual
|
||||
status.ActiveChannel = ""
|
||||
|
||||
ps.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
|
||||
// and sets status of given userId to dnd which will be restored back after endtime
|
||||
func (ps *PlatformService) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
|
||||
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := ps.GetStatus(userId)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userId, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
status.PrevStatus = status.Status
|
||||
status.Status = model.StatusDnd
|
||||
status.Manual = true
|
||||
|
||||
status.DNDEndTime = endtime
|
||||
|
||||
ps.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetStatusDoNotDisturb(userID string) {
|
||||
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := ps.GetStatus(userID)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
status.Status = model.StatusDnd
|
||||
status.Manual = true
|
||||
|
||||
ps.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) SetStatusOutOfOffice(userID string) {
|
||||
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := ps.GetStatus(userID)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOutOfOffice, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
status.Status = model.StatusOutOfOffice
|
||||
status.Manual = true
|
||||
|
||||
ps.SaveAndBroadcastStatus(status)
|
||||
}
|
||||
|
||||
func (ps *PlatformService) isUserAway(lastActivityAt int64) bool {
|
||||
return model.GetMillis()-lastActivityAt >= *ps.Config().TeamSettings.UserStatusAwayTimeout*1000
|
||||
}
|
||||
|
||||
@@ -72,15 +72,15 @@ type WebConnConfig struct {
|
||||
// It contains all the necessary state to manage sending/receiving data to/from
|
||||
// a websocket.
|
||||
type WebConn struct {
|
||||
sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically
|
||||
Platform *PlatformService
|
||||
Suite SuiteIFace
|
||||
PluginsEnvironment func() *plugin.Environment
|
||||
WebSocket *websocket.Conn
|
||||
T i18n.TranslateFunc
|
||||
Locale string
|
||||
Sequence int64
|
||||
UserId string
|
||||
sessionExpiresAt int64 // This should stay at the top for 64-bit alignment of 64-bit words accessed atomically
|
||||
Platform *PlatformService
|
||||
Suite SuiteIFace
|
||||
HookRunner HookRunner
|
||||
WebSocket *websocket.Conn
|
||||
T i18n.TranslateFunc
|
||||
Locale string
|
||||
Sequence int64
|
||||
UserId string
|
||||
|
||||
allChannelMembers map[string]string
|
||||
lastAllChannelMembersTime int64
|
||||
@@ -162,11 +162,11 @@ func (ps *PlatformService) PopulateWebConnConfig(s *model.Session, cfg *WebConnC
|
||||
}
|
||||
|
||||
// NewWebConn returns a new WebConn instance.
|
||||
func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, envFn func() *plugin.Environment) *WebConn {
|
||||
func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runner HookRunner) *WebConn {
|
||||
if cfg.Session.UserId != "" {
|
||||
ps.Go(func() {
|
||||
suite.SetStatusOnline(cfg.Session.UserId, false)
|
||||
suite.UpdateLastActivityAtIfNeeded(cfg.Session)
|
||||
ps.SetStatusOnline(cfg.Session.UserId, false)
|
||||
ps.UpdateLastActivityAtIfNeeded(cfg.Session)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, envF
|
||||
wc := &WebConn{
|
||||
Platform: ps,
|
||||
Suite: suite,
|
||||
PluginsEnvironment: envFn,
|
||||
HookRunner: runner,
|
||||
send: cfg.activeQueue,
|
||||
deadQueue: cfg.deadQueue,
|
||||
deadQueuePointer: cfg.deadQueuePointer,
|
||||
@@ -222,14 +222,12 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, envF
|
||||
wc.SetSessionExpiresAt(cfg.Session.ExpiresAt)
|
||||
wc.SetConnectionID(cfg.ConnectionID)
|
||||
|
||||
if pluginsEnvironment := wc.PluginsEnvironment(); pluginsEnvironment != nil {
|
||||
wc.Platform.Go(func() {
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnWebSocketConnect(wc.GetConnectionID(), wc.UserId)
|
||||
return true
|
||||
}, plugin.OnWebSocketConnectID)
|
||||
})
|
||||
}
|
||||
wc.Platform.Go(func() {
|
||||
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnWebSocketConnect(wc.GetConnectionID(), wc.UserId)
|
||||
return true
|
||||
}, plugin.OnWebSocketConnectID)
|
||||
})
|
||||
|
||||
return wc
|
||||
}
|
||||
@@ -238,12 +236,10 @@ func (wc *WebConn) pluginPostedConsumer(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for msg := range wc.pluginPosted {
|
||||
if pluginsEnvironment := wc.PluginsEnvironment(); pluginsEnvironment != nil {
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.WebSocketMessageHasBeenPosted(msg.connectionID, msg.userID, msg.req)
|
||||
return true
|
||||
}, plugin.WebSocketMessageHasBeenPostedID)
|
||||
}
|
||||
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.WebSocketMessageHasBeenPosted(msg.connectionID, msg.userID, msg.req)
|
||||
return true
|
||||
}, plugin.WebSocketMessageHasBeenPostedID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,14 +324,12 @@ func (wc *WebConn) Pump() {
|
||||
wc.Platform.HubUnregister(wc)
|
||||
close(wc.pumpFinished)
|
||||
|
||||
if pluginsEnvironment := wc.PluginsEnvironment(); pluginsEnvironment != nil {
|
||||
wc.Platform.Go(func() {
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnWebSocketDisconnect(wc.GetConnectionID(), wc.UserId)
|
||||
return true
|
||||
}, plugin.OnWebSocketDisconnectID)
|
||||
})
|
||||
}
|
||||
wc.Platform.Go(func() {
|
||||
wc.HookRunner.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnWebSocketDisconnect(wc.GetConnectionID(), wc.UserId)
|
||||
return true
|
||||
}, plugin.OnWebSocketDisconnectID)
|
||||
})
|
||||
}
|
||||
|
||||
func (wc *WebConn) readPump() {
|
||||
@@ -350,7 +344,7 @@ func (wc *WebConn) readPump() {
|
||||
}
|
||||
if wc.IsAuthenticated() {
|
||||
wc.Platform.Go(func() {
|
||||
wc.Suite.SetStatusAwayIfNeeded(wc.UserId, false)
|
||||
wc.Platform.SetStatusAwayIfNeeded(wc.UserId, false)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
@@ -753,15 +747,15 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
|
||||
return wc.GetConnectionID() == msg.GetBroadcast().ConnectionId
|
||||
}
|
||||
|
||||
if wc.GetConnectionID() == msg.GetBroadcast().OmitConnectionId {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the event is destined to a specific user
|
||||
if msg.GetBroadcast().UserId != "" {
|
||||
return wc.UserId == msg.GetBroadcast().UserId
|
||||
}
|
||||
|
||||
if wc.GetConnectionID() == msg.GetBroadcast().OmitConnectionId {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the user is omitted don't send the message
|
||||
if len(msg.GetBroadcast().OmitUsers) > 0 {
|
||||
if _, ok := msg.GetBroadcast().OmitUsers[wc.UserId]; ok {
|
||||
|
||||
@@ -5,6 +5,7 @@ package platform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -18,13 +19,27 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
)
|
||||
|
||||
type hookRunner struct {
|
||||
}
|
||||
|
||||
func (h *hookRunner) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
|
||||
|
||||
}
|
||||
func (h *hookRunner) HooksForPlugin(id string) (plugin.Hooks, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (h *hookRunner) GetPluginsEnvironment() *plugin.Environment {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWebConnAddDeadQueue(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
wc := th.Service.NewWebConn(&WebConnConfig{
|
||||
WebSocket: &websocket.Conn{},
|
||||
}, th.Suite, func() *plugin.Environment { return nil })
|
||||
}, th.Suite, &hookRunner{})
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
msg := &model.WebSocketEvent{}
|
||||
@@ -53,7 +68,7 @@ func TestWebConnIsInDeadQueue(t *testing.T) {
|
||||
|
||||
wc := th.Service.NewWebConn(&WebConnConfig{
|
||||
WebSocket: &websocket.Conn{},
|
||||
}, th.Suite, func() *plugin.Environment { return nil })
|
||||
}, th.Suite, &hookRunner{})
|
||||
|
||||
var i int
|
||||
for ; i < 2; i++ {
|
||||
@@ -114,7 +129,7 @@ func TestWebConnClearDeadQueue(t *testing.T) {
|
||||
|
||||
wc := th.Service.NewWebConn(&WebConnConfig{
|
||||
WebSocket: &websocket.Conn{},
|
||||
}, th.Suite, func() *plugin.Environment { return nil })
|
||||
}, th.Suite, &hookRunner{})
|
||||
|
||||
var i int
|
||||
for ; i < 2; i++ {
|
||||
@@ -140,7 +155,7 @@ func TestWebConnDrainDeadQueue(t *testing.T) {
|
||||
cfg := &WebConnConfig{
|
||||
WebSocket: c,
|
||||
}
|
||||
return th.Service.NewWebConn(cfg, th.Suite, func() *plugin.Environment { return nil })
|
||||
return th.Service.NewWebConn(cfg, th.Suite, &hookRunner{})
|
||||
}
|
||||
|
||||
t.Run("Empty Queue", func(t *testing.T) {
|
||||
|
||||
@@ -21,12 +21,6 @@ const (
|
||||
)
|
||||
|
||||
type SuiteIFace interface {
|
||||
SetStatusLastActivityAt(userID string, activityAt int64)
|
||||
SetStatusOffline(userID string, manual bool)
|
||||
IsUserAway(lastActivityAt int64) bool
|
||||
SetStatusOnline(userID string, manual bool)
|
||||
UpdateLastActivityAtIfNeeded(session model.Session)
|
||||
SetStatusAwayIfNeeded(userID string, manual bool)
|
||||
GetSession(token string) (*model.Session, *model.AppError)
|
||||
RolesGrantPermission(roleNames []string, permissionId string) bool
|
||||
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
|
||||
@@ -95,7 +89,7 @@ func newWebHub(ps *PlatformService) *Hub {
|
||||
}
|
||||
|
||||
// hubStart starts all the hubs.
|
||||
func (ps *PlatformService) hubStart(suite SuiteIFace) {
|
||||
func (ps *PlatformService) hubStart() {
|
||||
// Total number of hubs is twice the number of CPUs.
|
||||
numberOfHubs := runtime.NumCPU() * 2
|
||||
ps.logger.Info("Starting websocket hubs", mlog.Int("number_of_hubs", numberOfHubs))
|
||||
@@ -105,7 +99,7 @@ func (ps *PlatformService) hubStart(suite SuiteIFace) {
|
||||
for i := 0; i < numberOfHubs; i++ {
|
||||
hubs[i] = newWebHub(ps)
|
||||
hubs[i].connectionIndex = i
|
||||
hubs[i].Start(suite)
|
||||
hubs[i].Start()
|
||||
}
|
||||
// Assigning to the hubs slice without any mutex is fine because it is only assigned once
|
||||
// during the start of the program and always read from after that.
|
||||
@@ -366,7 +360,7 @@ func (h *Hub) Stop() {
|
||||
}
|
||||
|
||||
// Start starts the hub.
|
||||
func (h *Hub) Start(suite SuiteIFace) {
|
||||
func (h *Hub) Start() {
|
||||
var doStart func()
|
||||
var doRecoverableStart func()
|
||||
var doRecover func()
|
||||
@@ -439,7 +433,7 @@ func (h *Hub) Start(suite SuiteIFace) {
|
||||
conns := connIndex.ForUser(webConn.UserId)
|
||||
if len(conns) == 0 || areAllInactive(conns) {
|
||||
h.platform.Go(func() {
|
||||
suite.SetStatusOffline(webConn.UserId, false)
|
||||
h.platform.SetStatusOffline(webConn.UserId, false)
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -453,9 +447,9 @@ func (h *Hub) Start(suite SuiteIFace) {
|
||||
}
|
||||
}
|
||||
|
||||
if suite.IsUserAway(latestActivity) {
|
||||
if h.platform.isUserAway(latestActivity) {
|
||||
h.platform.Go(func() {
|
||||
suite.SetStatusLastActivityAt(webConn.UserId, latestActivity)
|
||||
h.platform.SetStatusLastActivityAt(webConn.UserId, latestActivity)
|
||||
})
|
||||
}
|
||||
case userID := <-h.invalidateUser:
|
||||
@@ -522,7 +516,7 @@ func (h *Hub) Start(suite SuiteIFace) {
|
||||
case <-h.stop:
|
||||
for webConn := range connIndex.All() {
|
||||
webConn.Close()
|
||||
suite.SetStatusOffline(webConn.UserId, false)
|
||||
h.platform.SetStatusOffline(webConn.UserId, false)
|
||||
}
|
||||
|
||||
h.explicitStop = true
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
platform_mocks "github.com/mattermost/mattermost-server/v6/app/platform/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/testlib"
|
||||
@@ -50,7 +49,7 @@ func registerDummyWebConn(t *testing.T, th *TestHelper, addr net.Addr, session *
|
||||
TFunc: i18n.IdentityTfunc(),
|
||||
Locale: "en",
|
||||
}
|
||||
wc := th.Service.NewWebConn(cfg, th.Suite, func() *plugin.Environment { return nil })
|
||||
wc := th.Service.NewWebConn(cfg, th.Suite, &hookRunner{})
|
||||
th.Service.HubRegister(wc)
|
||||
go wc.Pump()
|
||||
return wc
|
||||
@@ -68,7 +67,7 @@ func TestHubStopWithMultipleConnections(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
th.Service.Start(th.Suite)
|
||||
th.Service.Start()
|
||||
wc1 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
|
||||
wc2 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
|
||||
wc3 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
|
||||
@@ -91,7 +90,7 @@ func TestHubStopRaceCondition(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
th.Service.Start(th.Suite)
|
||||
th.Service.Start()
|
||||
wc1 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
|
||||
defer wc1.Close()
|
||||
|
||||
@@ -468,18 +467,13 @@ func TestHubIsRegistered(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
mockSuite := &platform_mocks.SuiteIFace{}
|
||||
mockSuite.On("SetStatusOnline", th.BasicUser.Id, false).Return()
|
||||
mockSuite.On("UpdateLastActivityAtIfNeeded", *session).Return()
|
||||
mockSuite.On("GetSession", session.Token).Return(session, nil)
|
||||
mockSuite.On("IsUserAway", mock.Anything).Return(false)
|
||||
mockSuite.On("SetStatusOffline", th.BasicUser.Id, false).Return()
|
||||
|
||||
th.Suite = mockSuite
|
||||
|
||||
s := httptest.NewServer(dummyWebsocketHandler(t))
|
||||
defer s.Close()
|
||||
|
||||
th.Service.Start(th.Suite)
|
||||
th.Service.Start()
|
||||
wc1 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
|
||||
wc2 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
|
||||
wc3 := registerDummyWebConn(t, th, s.Listener.Addr(), session)
|
||||
@@ -487,9 +481,7 @@ func TestHubIsRegistered(t *testing.T) {
|
||||
defer wc2.Close()
|
||||
defer wc3.Close()
|
||||
|
||||
session1 := wc1.session.Load().(*model.Session)
|
||||
|
||||
assert.True(t, th.Service.SessionIsRegistered(*session1))
|
||||
assert.True(t, th.Service.SessionIsRegistered(*wc1.session.Load().(*model.Session)))
|
||||
assert.True(t, th.Service.SessionIsRegistered(*wc2.session.Load().(*model.Session)))
|
||||
assert.True(t, th.Service.SessionIsRegistered(*wc3.session.Load().(*model.Session)))
|
||||
|
||||
@@ -552,7 +544,7 @@ func BenchmarkGetHubForUserId(b *testing.B) {
|
||||
th := Setup(b).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.Service.Start(th.Suite)
|
||||
th.Service.Start()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -59,8 +59,8 @@ func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketReque
|
||||
conn.Platform.HubRegister(conn)
|
||||
|
||||
conn.Platform.Go(func() {
|
||||
conn.Suite.SetStatusOnline(session.UserId, false)
|
||||
conn.Suite.UpdateLastActivityAtIfNeeded(*session)
|
||||
conn.Platform.SetStatusOnline(session.UserId, false)
|
||||
conn.Platform.UpdateLastActivityAtIfNeeded(*session)
|
||||
})
|
||||
|
||||
resp := model.NewWebSocketResponse(model.StatusOk, r.Seq, nil)
|
||||
|
||||
@@ -93,7 +93,7 @@ func (ch *Channels) SetPluginsEnvironment(pluginsEnvironment *plugin.Environment
|
||||
defer ch.pluginsLock.Unlock()
|
||||
|
||||
ch.pluginsEnvironment = pluginsEnvironment
|
||||
ch.srv.Platform().SetPluginsEnvironment(pluginsEnvironment)
|
||||
ch.srv.Platform().SetPluginsEnvironment(ch)
|
||||
}
|
||||
|
||||
func (ch *Channels) syncPluginsActiveState() {
|
||||
@@ -200,10 +200,6 @@ func (ch *Channels) syncPluginsActiveState() {
|
||||
if err := ch.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
|
||||
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) NewPluginAPI(c *request.Context, manifest *model.Manifest) plugin.API {
|
||||
@@ -217,7 +213,7 @@ func (a *App) InitPlugins(c *request.Context, pluginDir, webappPluginDir string)
|
||||
func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir string) {
|
||||
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
|
||||
defer func() {
|
||||
ch.srv.Platform().SetPluginsEnvironment(ch.pluginsEnvironment)
|
||||
ch.srv.Platform().SetPluginsEnvironment(ch)
|
||||
}()
|
||||
|
||||
ch.pluginsLock.RLock()
|
||||
@@ -247,7 +243,15 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s
|
||||
return New(ServerConnector(ch)).NewPluginAPI(c, manifest)
|
||||
}
|
||||
|
||||
env, err := plugin.NewEnvironment(newAPIFunc, NewDriverImpl(ch.srv), pluginDir, webappPluginDir, ch.srv.Log(), ch.srv.GetMetrics())
|
||||
env, err := plugin.NewEnvironment(
|
||||
newAPIFunc,
|
||||
NewDriverImpl(ch.srv),
|
||||
pluginDir,
|
||||
webappPluginDir,
|
||||
*ch.cfgSvc.Config().ExperimentalSettings.PatchPluginsReactDOM,
|
||||
ch.srv.Log(),
|
||||
ch.srv.GetMetrics(),
|
||||
)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
@@ -282,14 +286,13 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s
|
||||
ch.installFeatureFlagPlugins()
|
||||
ch.syncPluginsActiveState()
|
||||
}
|
||||
if pluginsEnvironment := ch.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
if err := hooks.OnConfigurationChange(); err != nil {
|
||||
ch.srv.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err))
|
||||
}
|
||||
return true
|
||||
}, plugin.OnConfigurationChangeID)
|
||||
}
|
||||
|
||||
ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
if err := hooks.OnConfigurationChange(); err != nil {
|
||||
ch.srv.Log().Error("Plugin OnConfigurationChange hook failed", mlog.Err(err))
|
||||
}
|
||||
return true
|
||||
}, plugin.OnConfigurationChangeID)
|
||||
})
|
||||
ch.pluginsLock.Unlock()
|
||||
|
||||
@@ -422,11 +425,6 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
// activation if inactive anywhere in the cluster.
|
||||
// Notifies cluster peers through config change.
|
||||
func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
appErr := a.checkIfIntegrationsMeetFreemiumLimits([]string{id})
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
return a.ch.enablePlugin(id)
|
||||
}
|
||||
|
||||
@@ -537,20 +535,6 @@ func (ch *Channels) disablePlugin(id string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ch *Channels) notifyIntegrationsUsageChanged() *model.AppError {
|
||||
usage, appErr := ch.getIntegrationsUsage()
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventIntegrationsUsageChanged, "", "", "", nil, "")
|
||||
message.Add("usage", usage)
|
||||
message.GetBroadcast().ContainsSensitiveData = true
|
||||
ch.Publish(message)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
|
||||
@@ -1237,3 +1237,27 @@ func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) {
|
||||
func (api *PluginAPI) RegisterCollectionAndTopic(collectionType, topicType string) error {
|
||||
return api.app.registerCollectionAndTopic(api.id, collectionType, topicType)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) {
|
||||
us, err := api.app.CreateUploadSession(api.ctx, us)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return us, nil
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) {
|
||||
fi, err := api.app.UploadData(api.ctx, us, rd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUploadSession(uploadID string) (*model.UploadSession, error) {
|
||||
fi, err := api.app.GetUploadSession(uploadID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func setupMultiPluginAPITest(t *testing.T, pluginCodes []string, pluginManifests
|
||||
return app.NewPluginAPI(c, manifest)
|
||||
}
|
||||
|
||||
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil)
|
||||
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(pluginCodes), len(pluginIDs))
|
||||
@@ -849,7 +849,7 @@ func TestPluginAPIGetPlugins(t *testing.T) {
|
||||
defer os.RemoveAll(pluginDir)
|
||||
defer os.RemoveAll(webappPluginDir)
|
||||
|
||||
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), nil)
|
||||
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
pluginIDs := []string{"pluginid1", "pluginid2", "pluginid3"}
|
||||
@@ -937,7 +937,7 @@ func TestInstallPlugin(t *testing.T) {
|
||||
return app.NewPluginAPI(c, manifest)
|
||||
}
|
||||
|
||||
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil)
|
||||
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
app.ch.SetPluginsEnvironment(env)
|
||||
@@ -1632,7 +1632,7 @@ func TestAPIMetrics(t *testing.T) {
|
||||
defer os.RemoveAll(pluginDir)
|
||||
defer os.RemoveAll(webappPluginDir)
|
||||
|
||||
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), metricsMock)
|
||||
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.App.ch.SetPluginsEnvironment(env)
|
||||
@@ -2079,7 +2079,7 @@ func TestRegisterCollectionAndTopic(t *testing.T) {
|
||||
return th.App.NewPluginAPI(th.Context, manifest)
|
||||
}
|
||||
|
||||
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, th.App.Log(), nil)
|
||||
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.App.ch.SetPluginsEnvironment(env)
|
||||
@@ -2100,3 +2100,98 @@ func TestRegisterCollectionAndTopic(t *testing.T) {
|
||||
err = api.RegisterCollectionAndTopic("some other collection", "topicToBeRepeated")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPluginUploadsAPI(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
pluginCode := fmt.Sprintf(`
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
)
|
||||
|
||||
type TestPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func (p *TestPlugin) OnActivate() error {
|
||||
data := []byte("some content to upload")
|
||||
us, err := p.API.CreateUploadSession(&model.UploadSession{
|
||||
Id: "%s",
|
||||
UserId: "%s",
|
||||
ChannelId: "%s",
|
||||
Type: model.UploadTypeAttachment,
|
||||
FileSize: int64(len(data)),
|
||||
Filename: "upload.test",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create upload session: %%w", err)
|
||||
}
|
||||
|
||||
us2, err := p.API.GetUploadSession(us.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get upload session: %%w", err)
|
||||
}
|
||||
|
||||
if us.Id != us2.Id {
|
||||
return fmt.Errorf("upload sessions should match")
|
||||
}
|
||||
|
||||
fi, err := p.API.UploadData(us, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload data: %%w", err)
|
||||
}
|
||||
|
||||
if fi == nil || fi.Id == "" {
|
||||
return fmt.Errorf("fileinfo should be set")
|
||||
}
|
||||
|
||||
fileData, appErr := p.API.GetFile(fi.Id)
|
||||
if appErr != nil {
|
||||
return fmt.Errorf("failed to get file data: %%w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data, fileData) {
|
||||
return fmt.Errorf("file data should match")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&TestPlugin{})
|
||||
}
|
||||
`, model.NewId(), th.BasicUser.Id, th.BasicChannel.Id)
|
||||
|
||||
pluginDir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
webappPluginDir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(pluginDir)
|
||||
defer os.RemoveAll(webappPluginDir)
|
||||
|
||||
newPluginAPI := func(manifest *model.Manifest) plugin.API {
|
||||
return th.App.NewPluginAPI(th.Context, manifest)
|
||||
}
|
||||
env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, false, th.App.Log(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.App.ch.SetPluginsEnvironment(env)
|
||||
|
||||
pluginID := "testplugin"
|
||||
pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}`
|
||||
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
|
||||
utils.CompileGo(t, pluginCode, backend)
|
||||
|
||||
os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600)
|
||||
manifest, activated, reterr := env.Activate(pluginID)
|
||||
require.NoError(t, reterr)
|
||||
require.NotNil(t, manifest)
|
||||
require.True(t, activated)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, a
|
||||
webappPluginDir, err := os.MkdirTemp("", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, app.Log(), nil)
|
||||
env, err := plugin.NewEnvironment(apiFunc, NewDriverImpl(app.Srv()), pluginDir, webappPluginDir, false, app.Log(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
app.ch.SetPluginsEnvironment(env)
|
||||
@@ -1030,7 +1030,7 @@ func TestHookMetrics(t *testing.T) {
|
||||
defer os.RemoveAll(pluginDir)
|
||||
defer os.RemoveAll(webappPluginDir)
|
||||
|
||||
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), metricsMock)
|
||||
env, err := plugin.NewEnvironment(th.NewPluginAPI, NewDriverImpl(th.Server), pluginDir, webappPluginDir, false, th.App.Log(), metricsMock)
|
||||
require.NoError(t, err)
|
||||
|
||||
th.App.ch.SetPluginsEnvironment(env)
|
||||
@@ -1234,7 +1234,7 @@ func TestHookRunDataRetention(t *testing.T) {
|
||||
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
||||
|
||||
hookCalled := false
|
||||
th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
n, _ := hooks.RunDataRetention(0, 0)
|
||||
// Ensure return it correct
|
||||
assert.Equal(t, int64(100), n)
|
||||
@@ -1278,7 +1278,7 @@ func TestHookOnSendDailyTelemetry(t *testing.T) {
|
||||
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
||||
|
||||
hookCalled := false
|
||||
th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnSendDailyTelemetry()
|
||||
|
||||
hookCalled = true
|
||||
@@ -1322,7 +1322,7 @@ func TestHookOnCloudLimitsUpdated(t *testing.T) {
|
||||
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
||||
|
||||
hookCalled := false
|
||||
th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
th.App.Channels().RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnCloudLimitsUpdated(nil)
|
||||
|
||||
hookCalled = true
|
||||
|
||||
@@ -102,10 +102,6 @@ func (ch *Channels) installPluginFromData(data model.PluginEventData) {
|
||||
if err := ch.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("Failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
|
||||
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channels) removePluginFromData(data model.PluginEventData) {
|
||||
@@ -118,10 +114,6 @@ func (ch *Channels) removePluginFromData(data model.PluginEventData) {
|
||||
if err := ch.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Warn("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
|
||||
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
// InstallPluginWithSignature verifies and installs plugin.
|
||||
@@ -177,10 +169,6 @@ func (ch *Channels) installPlugin(pluginFile, signature io.ReadSeeker, installat
|
||||
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
|
||||
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
@@ -455,10 +443,6 @@ func (ch *Channels) RemovePlugin(id string) *model.AppError {
|
||||
mlog.Warn("Failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
if err := ch.notifyIntegrationsUsageChanged(); err != nil {
|
||||
mlog.Warn("Failed to notify integrations usage changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1057,17 +1057,6 @@ func TestEnablePluginWithCloudLimits(t *testing.T) {
|
||||
appErr = th.App.EnablePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.EnablePlugin("testplugin2")
|
||||
checkError(t, appErr)
|
||||
require.Equal(t, "app.install_integration.reached_max_limit.error", appErr.Id)
|
||||
|
||||
th.App.Srv().RemoveLicense()
|
||||
appErr = th.App.EnablePlugin("testplugin2")
|
||||
checkNoError(t, appErr)
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
appErr = th.App.EnablePlugin("testplugin2")
|
||||
checkError(t, appErr)
|
||||
|
||||
// Let enable succeed if a CWS error occurs
|
||||
cloud = &mocks.CloudInterface{}
|
||||
th.App.Srv().Cloud = cloud
|
||||
|
||||
270
app/post.go
270
app/post.go
@@ -267,30 +267,40 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
|
||||
}
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var rejectionError *model.AppError
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post.ForPlugin())
|
||||
if rejectionReason != "" {
|
||||
id := "Post rejected by plugin. " + rejectionReason
|
||||
if rejectionReason == plugin.DismissPostError {
|
||||
id = plugin.DismissPostError
|
||||
}
|
||||
rejectionError = model.NewAppError("createPost", id, nil, "", http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if replacementPost != nil {
|
||||
// the original post's metadata (if there ever was any) is lost, and will be rebuilt.
|
||||
post = replacementPost
|
||||
}
|
||||
if !a.isPostPriorityEnabled() && post.GetPriority() != nil {
|
||||
post.Metadata.Priority = nil
|
||||
}
|
||||
|
||||
return true
|
||||
}, plugin.MessageWillBePostedID)
|
||||
|
||||
if rejectionError != nil {
|
||||
return nil, rejectionError
|
||||
var metadata *model.PostMetadata
|
||||
if post.Metadata != nil {
|
||||
metadata = post.Metadata.Copy()
|
||||
}
|
||||
var rejectionError *model.AppError
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
replacementPost, rejectionReason := hooks.MessageWillBePosted(pluginContext, post.ForPlugin())
|
||||
if rejectionReason != "" {
|
||||
id := "Post rejected by plugin. " + rejectionReason
|
||||
if rejectionReason == plugin.DismissPostError {
|
||||
id = plugin.DismissPostError
|
||||
}
|
||||
rejectionError = model.NewAppError("createPost", id, nil, "", http.StatusBadRequest)
|
||||
return false
|
||||
}
|
||||
if replacementPost != nil {
|
||||
post = replacementPost
|
||||
if post.Metadata != nil && metadata != nil {
|
||||
post.Metadata.Priority = metadata.Priority
|
||||
} else {
|
||||
post.Metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, plugin.MessageWillBePostedID)
|
||||
|
||||
if rejectionError != nil {
|
||||
return nil, rejectionError
|
||||
}
|
||||
|
||||
// Pre-fill the CreateAt field for link previews to get the correct timestamp.
|
||||
@@ -324,16 +334,13 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
|
||||
|
||||
// We make a copy of the post for the plugin hook to avoid a race condition,
|
||||
// and to remove the non-GOB-encodable Metadata from it.
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
pluginPost := rpost.ForPlugin()
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenPosted(pluginContext, pluginPost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenPostedID)
|
||||
})
|
||||
}
|
||||
pluginPost := rpost.ForPlugin()
|
||||
a.Srv().Go(func() {
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenPosted(pluginContext, pluginPost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenPostedID)
|
||||
})
|
||||
|
||||
if a.Metrics() != nil {
|
||||
a.Metrics().IncrementPostCreate()
|
||||
@@ -351,7 +358,9 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
|
||||
|
||||
// Normally, we would let the API layer call PreparePostForClient, but we do it here since it also needs
|
||||
// to be done when we send the post over the websocket in handlePostEvents
|
||||
rpost = a.PreparePostForClient(c, rpost, true, false)
|
||||
// PS: we don't want to include PostPriority from the db to avoid the replica lag,
|
||||
// so we just return the one that was passed with post
|
||||
rpost = a.PreparePostForClient(c, rpost, true, false, false)
|
||||
|
||||
// Make sure poster is following the thread
|
||||
if *a.Config().ServiceSettings.ThreadAutoFollow && rpost.RootId != "" {
|
||||
@@ -523,7 +532,7 @@ func (a *App) SendEphemeralPost(c request.CTX, userID string, post *model.Post)
|
||||
|
||||
post.GenerateActionIds()
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", post.ChannelId, userID, nil, "")
|
||||
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false)
|
||||
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true)
|
||||
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
|
||||
|
||||
postJSON, jsonErr := post.ToJSON()
|
||||
@@ -546,7 +555,7 @@ func (a *App) UpdateEphemeralPost(c request.CTX, userID string, post *model.Post
|
||||
|
||||
post.GenerateActionIds()
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventPostEdited, "", post.ChannelId, userID, nil, "")
|
||||
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false)
|
||||
post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true)
|
||||
post = model.AddPostActionCookies(post, a.PostActionCookieSecret())
|
||||
postJSON, jsonErr := post.ToJSON()
|
||||
if jsonErr != nil {
|
||||
@@ -652,20 +661,18 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool)
|
||||
oldPost.RemoteId = model.NewString(*post.RemoteId)
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var rejectionReason string
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin())
|
||||
return post != nil
|
||||
}, plugin.MessageWillBeUpdatedID)
|
||||
if newPost == nil {
|
||||
return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
}
|
||||
// Restore the post metadata that was stripped by the plugin. Set it to
|
||||
// the last known good.
|
||||
newPost.Metadata = oldPost.Metadata
|
||||
var rejectionReason string
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost.ForPlugin(), oldPost.ForPlugin())
|
||||
return post != nil
|
||||
}, plugin.MessageWillBeUpdatedID)
|
||||
if newPost == nil {
|
||||
return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
}
|
||||
// Restore the post metadata that was stripped by the plugin. Set it to
|
||||
// the last known good.
|
||||
newPost.Metadata = oldPost.Metadata
|
||||
|
||||
rpost, nErr := a.Srv().Store().Post().Update(newPost, oldPost)
|
||||
if nErr != nil {
|
||||
@@ -678,19 +685,16 @@ func (a *App) UpdatePost(c *request.Context, post *model.Post, safeUpdate bool)
|
||||
}
|
||||
}
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
pluginOldPost := oldPost.ForPlugin()
|
||||
pluginNewPost := newPost.ForPlugin()
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenUpdated(pluginContext, pluginNewPost, pluginOldPost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenUpdatedID)
|
||||
})
|
||||
}
|
||||
pluginOldPost := oldPost.ForPlugin()
|
||||
pluginNewPost := newPost.ForPlugin()
|
||||
a.Srv().Go(func() {
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenUpdated(pluginContext, pluginNewPost, pluginOldPost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenUpdatedID)
|
||||
})
|
||||
|
||||
rpost = a.PreparePostForClientWithEmbedsAndImages(c, rpost, false, true)
|
||||
rpost = a.PreparePostForClientWithEmbedsAndImages(c, rpost, false, true, true)
|
||||
|
||||
// Ensure IsFollowing is nil since this updated post will be broadcast to all users
|
||||
// and we don't want to have to populate it for every single user and broadcast to each
|
||||
@@ -1449,6 +1453,24 @@ func (a *App) ComputeLastAccessiblePostTime() error {
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
// All posts are accessible - we must check if a previous value was set so we can clear it
|
||||
systemValue, err := a.Srv().Store().System().GetByName(model.SystemLastAccessiblePostTime)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
// There was no previous value, nothing to do
|
||||
return nil
|
||||
default:
|
||||
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.get_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
if systemValue != nil {
|
||||
// Previous value was set, so we must clear it
|
||||
if _, err = a.Srv().Store().System().PermanentDeleteByName(model.SystemLastAccessiblePostTime); err != nil {
|
||||
return model.NewAppError("ComputeLastAccessiblePostTime", "app.system.permanent_delete_by_name.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
// Cloud limit is not applicable
|
||||
return nil
|
||||
}
|
||||
@@ -1695,7 +1717,7 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P
|
||||
|
||||
posts, nErr := a.Srv().Store().Post().GetPostsByThread(post.Id, timestamp)
|
||||
if nErr != nil {
|
||||
return 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
count := 0
|
||||
@@ -1722,7 +1744,7 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P
|
||||
|
||||
groups, nErr := a.getGroupsAllowedForReferenceInChannel(channel, team)
|
||||
if nErr != nil {
|
||||
return 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
for _, p := range posts {
|
||||
@@ -1739,25 +1761,33 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P
|
||||
|
||||
// countMentionsFromPost returns the number of posts in the post's channel that mention the user after and including the
|
||||
// given post.
|
||||
func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model.Post) (int, int, *model.AppError) {
|
||||
func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model.Post) (int, int, int, *model.AppError) {
|
||||
channel, err := a.GetChannel(c, post.ChannelId)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
if channel.Type == model.ChannelTypeDirect {
|
||||
// In a DM channel, every post made by the other user is a mention
|
||||
count, countRoot, nErr := a.Srv().Store().Channel().CountPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
|
||||
if nErr != nil {
|
||||
return 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
return count, countRoot, nil
|
||||
var urgentCount int
|
||||
if a.isPostPriorityEnabled() {
|
||||
urgentCount, nErr = a.Srv().Store().Channel().CountUrgentPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
|
||||
if nErr != nil {
|
||||
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_urgent_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
return count, countRoot, urgentCount, nil
|
||||
}
|
||||
|
||||
channelMember, err := a.GetChannelMember(c, channel.Id, user.Id)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
keywords := addMentionKeywordsForUser(
|
||||
@@ -1775,15 +1805,25 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
|
||||
|
||||
thread, err := a.GetPostThread(post.Id, model.GetPostsOptions{}, user.Id)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
count := 0
|
||||
countRoot := 0
|
||||
urgentCount := 0
|
||||
if isPostMention(user, post, keywords, thread.Posts, mentionedByThread, checkForCommentMentions) {
|
||||
count += 1
|
||||
if post.RootId == "" {
|
||||
countRoot += 1
|
||||
if a.isPostPriorityEnabled() {
|
||||
priority, err := a.GetPriorityForPost(post.Id)
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
if priority != nil && *priority.Priority == model.PostPriorityUrgent {
|
||||
urgentCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1797,18 +1837,32 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
|
||||
PerPage: perPage,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
mentionPostIds := make([]string, 0)
|
||||
for _, postID := range postList.Order {
|
||||
if isPostMention(user, postList.Posts[postID], keywords, postList.Posts, mentionedByThread, checkForCommentMentions) {
|
||||
count += 1
|
||||
if postList.Posts[postID].RootId == "" {
|
||||
mentionPostIds = append(mentionPostIds, postID)
|
||||
countRoot += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if a.isPostPriorityEnabled() {
|
||||
priorityList, nErr := a.Srv().Store().PostPriority().GetForPosts(mentionPostIds)
|
||||
if nErr != nil {
|
||||
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.get_priority_for_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
for _, priority := range priorityList {
|
||||
if *priority.Priority == model.PostPriorityUrgent {
|
||||
urgentCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(postList.Order) < perPage {
|
||||
break
|
||||
}
|
||||
@@ -1816,7 +1870,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
|
||||
page += 1
|
||||
}
|
||||
|
||||
return count, countRoot, nil
|
||||
return count, countRoot, urgentCount, nil
|
||||
}
|
||||
|
||||
func isCommentMention(user *model.User, post *model.Post, otherPosts map[string]*model.Post, mentionedByThread map[string]bool) bool {
|
||||
@@ -2015,7 +2069,7 @@ func (a *App) SetPostReminder(postID, userID string, targetTime int64) *model.Ap
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventEphemeralMessage, "", ephemeralPost.ChannelId, userID, nil, "")
|
||||
ephemeralPost = a.PreparePostForClientWithEmbedsAndImages(request.EmptyContext(a.Log()), ephemeralPost, true, false)
|
||||
ephemeralPost = a.PreparePostForClientWithEmbedsAndImages(request.EmptyContext(a.Log()), ephemeralPost, true, false, true)
|
||||
ephemeralPost = model.AddPostActionCookies(ephemeralPost, a.PostActionCookieSecret())
|
||||
|
||||
postJSON, jsonErr := ephemeralPost.ToJSON()
|
||||
@@ -2095,9 +2149,77 @@ func (a *App) CheckPostReminders() {
|
||||
|
||||
}
|
||||
|
||||
func (a *App) GetPostInfo(c request.CTX, postID string) (*model.PostInfo, *model.AppError) {
|
||||
userID := c.Session().UserId
|
||||
post, appErr := a.GetSinglePost(postID, false)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
channel, appErr := a.GetChannel(c, post.ChannelId)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
notFoundError := model.NewAppError("GetPostInfo", "app.post.get.app_error", nil, "", http.StatusNotFound)
|
||||
|
||||
var team *model.Team
|
||||
hasPermissionToAccessTeam := false
|
||||
if channel.TeamId != "" {
|
||||
team, appErr = a.GetTeam(channel.TeamId)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if team.Type == model.TeamOpen {
|
||||
hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPublicTeams)
|
||||
} else if team.Type == model.TeamInvite {
|
||||
hasPermissionToAccessTeam = a.HasPermissionToTeam(userID, team.Id, model.PermissionJoinPrivateTeams)
|
||||
}
|
||||
} else {
|
||||
// This happens in case of DMs and GMs.
|
||||
hasPermissionToAccessTeam = true
|
||||
}
|
||||
|
||||
if !hasPermissionToAccessTeam {
|
||||
return nil, notFoundError
|
||||
}
|
||||
|
||||
hasPermissionToAccessChannel := false
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
hasPermissionToAccessChannel = true
|
||||
} else if channel.Type == model.ChannelTypePrivate {
|
||||
hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionManagePrivateChannelMembers)
|
||||
} else if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
|
||||
hasPermissionToAccessChannel = a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannel)
|
||||
}
|
||||
|
||||
if !hasPermissionToAccessChannel {
|
||||
return nil, notFoundError
|
||||
}
|
||||
|
||||
_, channelMemberErr := a.GetChannelMember(c, channel.Id, userID)
|
||||
|
||||
info := model.PostInfo{
|
||||
ChannelId: channel.Id,
|
||||
ChannelType: channel.Type,
|
||||
ChannelDisplayName: channel.DisplayName,
|
||||
HasJoinedChannel: channelMemberErr == nil,
|
||||
}
|
||||
if team != nil {
|
||||
_, teamMemberErr := a.GetTeamMember(team.Id, userID)
|
||||
|
||||
info.TeamId = team.Id
|
||||
info.TeamType = team.Type
|
||||
info.TeamDisplayName = team.DisplayName
|
||||
info.HasJoinedTeam = teamMemberErr == nil
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThreadList, userID string) (*model.TopThreadList, error) {
|
||||
for _, topThread := range topThreadList.Items {
|
||||
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false)
|
||||
topThread.Post = a.PreparePostForClientWithEmbedsAndImages(c, topThread.Post, false, false, true)
|
||||
sanitizedPost, err := a.SanitizePostMetadataForUser(c, topThread.Post, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2106,3 +2228,7 @@ func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThrea
|
||||
}
|
||||
return topThreadList, nil
|
||||
}
|
||||
|
||||
func (a *App) isPostPriorityEnabled() bool {
|
||||
return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority
|
||||
}
|
||||
|
||||
130
app/post_acknowledgements.go
Обычный файл
130
app/post_acknowledgements.go
Обычный файл
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
)
|
||||
|
||||
func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError) {
|
||||
post, err := a.GetSinglePost(postID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channel, err := a.GetChannel(c, post.ChannelId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if channel.DeleteAt > 0 {
|
||||
return nil, model.NewAppError("SaveAcknowledgementForPost", "api.acknowledgement.save.archived_channel.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
acknowledgedAt := model.GetMillis()
|
||||
acknowledgement, nErr := a.Srv().Store().PostAcknowledgement().Save(postID, userID, acknowledgedAt)
|
||||
|
||||
if nErr != nil {
|
||||
var appErr *model.AppError
|
||||
switch {
|
||||
case errors.As(nErr, &appErr):
|
||||
return nil, appErr
|
||||
default:
|
||||
return nil, model.NewAppError("SaveAcknowledgementForPost", "app.acknowledgement.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementAdded, acknowledgement, post)
|
||||
})
|
||||
|
||||
return acknowledgement, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError {
|
||||
post, err := a.GetSinglePost(postID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel, err := a.GetChannel(c, post.ChannelId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if channel.DeleteAt > 0 {
|
||||
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.archived_channel.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
oldAck, nErr := a.Srv().Store().PostAcknowledgement().Get(postID, userID)
|
||||
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(nErr, &nfErr):
|
||||
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
|
||||
default:
|
||||
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
}
|
||||
|
||||
if model.GetMillis()-oldAck.AcknowledgedAt > 5*60*1000 {
|
||||
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.deadline.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
nErr = a.Srv().Store().PostAcknowledgement().Delete(oldAck)
|
||||
if nErr != nil {
|
||||
return model.NewAppError("DeleteAcknowledgementForPost", "app.acknowledgement.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementRemoved, oldAck, post)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
|
||||
acknowledgements, nErr := a.Srv().Store().PostAcknowledgement().GetForPost(postID)
|
||||
if nErr != nil {
|
||||
return nil, model.NewAppError("GetAcknowledgementsForPost", "app.acknowledgement.getforpost.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
return acknowledgements, nil
|
||||
}
|
||||
|
||||
func (a *App) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
|
||||
acknowledgements, err := a.Srv().Store().PostAcknowledgement().GetForPosts(postList.Order)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPostAcknowledgementsForPostList", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
acknowledgementsMap := make(map[string][]*model.PostAcknowledgement)
|
||||
|
||||
for _, ack := range acknowledgements {
|
||||
acknowledgementsMap[ack.PostId] = append(acknowledgementsMap[ack.PostId], ack)
|
||||
}
|
||||
|
||||
return acknowledgementsMap, nil
|
||||
}
|
||||
|
||||
func (a *App) sendAcknowledgementEvent(event string, acknowledgement *model.PostAcknowledgement, post *model.Post) {
|
||||
// send out that a acknowledgement has been added/removed
|
||||
message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil, "")
|
||||
|
||||
acknowledgementJSON, err := json.Marshal(acknowledgement)
|
||||
if err != nil {
|
||||
a.Log().Warn("Failed to encode acknowledgement to JSON", mlog.Err(err))
|
||||
}
|
||||
message.Add("acknowledgement", string(acknowledgementJSON))
|
||||
a.Publish(message)
|
||||
}
|
||||
149
app/post_acknowledgements_test.go
Обычный файл
149
app/post_acknowledgements_test.go
Обычный файл
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPostAcknowledgementsApp(t *testing.T) {
|
||||
t.Run("SaveAcknowledgementForPost", func(t *testing.T) { testSaveAcknowledgementForPost(t) })
|
||||
t.Run("DeleteAcknowledgementForPost", func(t *testing.T) { testDeleteAcknowledgementForPost(t) })
|
||||
t.Run("GetAcknowledgementsForPostList", func(t *testing.T) { testGetAcknowledgementsForPostList(t) })
|
||||
}
|
||||
|
||||
func testSaveAcknowledgementForPost(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
t.Run("save acknowledgment for post should save acknowledgement", func(t *testing.T) {
|
||||
post, err := th.App.CreatePostAsUser(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "message",
|
||||
}, "", true)
|
||||
|
||||
require.Nil(t, err)
|
||||
|
||||
acknowledgment, err := th.App.SaveAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Greater(t, acknowledgment.AcknowledgedAt, int64(0))
|
||||
require.Equal(t, post.Id, acknowledgment.PostId)
|
||||
require.Equal(t, th.BasicUser.Id, acknowledgment.UserId)
|
||||
})
|
||||
}
|
||||
|
||||
func testDeleteAcknowledgementForPost(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
post, err := th.App.CreatePostAsUser(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
}, "", true)
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("delete acknowledgment for post should delete acknowledgement", func(t *testing.T) {
|
||||
_, err = th.App.SaveAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
acknowledgments, err := th.App.GetAcknowledgementsForPost(post.Id)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, acknowledgments, 1)
|
||||
require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0))
|
||||
|
||||
err = th.App.DeleteAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
acknowledgments, err = th.App.GetAcknowledgementsForPost(post.Id)
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, acknowledgments)
|
||||
})
|
||||
|
||||
t.Run("delete acknowledgment for post after 5 min after acknowledged should not delete", func(t *testing.T) {
|
||||
_, nErr := th.App.Srv().Store().PostAcknowledgement().Save(post.Id, th.BasicUser.Id, model.GetMillis()-int64(6*60*1000))
|
||||
require.NoError(t, nErr)
|
||||
|
||||
acknowledgments, err := th.App.GetAcknowledgementsForPost(post.Id)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, acknowledgments, 1)
|
||||
require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0))
|
||||
|
||||
err = th.App.DeleteAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, 403, err.StatusCode)
|
||||
|
||||
acknowledgments, err = th.App.GetAcknowledgementsForPost(post.Id)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, acknowledgments, 1)
|
||||
require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0))
|
||||
})
|
||||
}
|
||||
|
||||
func testGetAcknowledgementsForPostList(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
p1, err := th.App.CreatePostAsUser(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
}, "", true)
|
||||
require.Nil(t, err)
|
||||
|
||||
p2, err := th.App.CreatePostAsUser(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
}, "", true)
|
||||
require.Nil(t, err)
|
||||
|
||||
p3, err := th.App.CreatePostAsUser(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
}, "", true)
|
||||
require.Nil(t, err)
|
||||
|
||||
t.Run("get acknowledgments for post list should return a map", func(t *testing.T) {
|
||||
_, err = th.App.SaveAcknowledgementForPost(th.Context, p1.Id, th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
_, err = th.App.SaveAcknowledgementForPost(th.Context, p2.Id, th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
_, err = th.App.SaveAcknowledgementForPost(th.Context, p1.Id, th.BasicUser2.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
postList := model.NewPostList()
|
||||
postList.AddPost(p1)
|
||||
postList.AddOrder(p1.Id)
|
||||
postList.AddPost(p2)
|
||||
postList.AddOrder(p2.Id)
|
||||
postList.AddPost(p3)
|
||||
postList.AddOrder(p3.Id)
|
||||
|
||||
acks1, err := th.App.GetAcknowledgementsForPost(p1.Id)
|
||||
require.Nil(t, err)
|
||||
acks2, err := th.App.GetAcknowledgementsForPost(p2.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
acknowledgementsMap, err := th.App.GetAcknowledgementsForPostList(postList)
|
||||
require.Nil(t, err)
|
||||
|
||||
expected := map[string][]*model.PostAcknowledgement{
|
||||
p1.Id: acks1,
|
||||
p2.Id: acks2,
|
||||
}
|
||||
require.Equal(t, expected, acknowledgementsMap)
|
||||
require.Len(t, acknowledgementsMap[p1.Id], 2)
|
||||
require.Len(t, acknowledgementsMap[p2.Id], 1)
|
||||
require.Nil(t, acknowledgementsMap[p3.Id])
|
||||
})
|
||||
}
|
||||
@@ -56,11 +56,25 @@ func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostLi
|
||||
}
|
||||
|
||||
for id, originalPost := range originalList.Posts {
|
||||
post := a.PreparePostForClientWithEmbedsAndImages(c, originalPost, false, false)
|
||||
post := a.PreparePostForClientWithEmbedsAndImages(c, originalPost, false, false, false)
|
||||
|
||||
list.Posts[id] = post
|
||||
}
|
||||
|
||||
if a.isPostPriorityEnabled() {
|
||||
priority, _ := a.GetPriorityForPostList(list)
|
||||
acknowledgements, _ := a.GetAcknowledgementsForPostList(list)
|
||||
|
||||
for _, id := range list.Order {
|
||||
if _, ok := priority[id]; ok {
|
||||
list.Posts[id].Metadata.Priority = priority[id]
|
||||
}
|
||||
if _, ok := acknowledgements[id]; ok {
|
||||
list.Posts[id].Metadata.Acknowledgements = acknowledgements[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -90,7 +104,7 @@ func (a *App) OverrideIconURLIfEmoji(c request.CTX, post *model.Post) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post {
|
||||
func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post {
|
||||
post := originalPost.Clone()
|
||||
|
||||
// Proxy image links before constructing metadata so that requests go through the proxy
|
||||
@@ -123,11 +137,27 @@ func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNe
|
||||
post.Metadata.Files = fileInfos
|
||||
}
|
||||
|
||||
if includePriority && a.isPostPriorityEnabled() && post.RootId == "" {
|
||||
// Post's Priority if any
|
||||
if priority, err := a.GetPriorityForPost(post.Id); err != nil {
|
||||
mlog.Warn("Failed to get post priority for a post", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
} else {
|
||||
post.Metadata.Priority = priority
|
||||
}
|
||||
|
||||
// Post's acknowledgements if any
|
||||
if acknowledgements, err := a.GetAcknowledgementsForPost(post.Id); err != nil {
|
||||
mlog.Warn("Failed to get post acknowledgements for a post", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
} else {
|
||||
post.Metadata.Acknowledgements = acknowledgements
|
||||
}
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
func (a *App) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost bool) *model.Post {
|
||||
post := a.PreparePostForClient(c, originalPost, isNewPost, isEditPost)
|
||||
func (a *App) PreparePostForClientWithEmbedsAndImages(c request.CTX, originalPost *model.Post, isNewPost, isEditPost, includePriority bool) *model.Post {
|
||||
post := a.PreparePostForClient(c, originalPost, isNewPost, isEditPost, includePriority)
|
||||
post = a.getEmbedsAndImages(c, post, isNewPost)
|
||||
return post
|
||||
}
|
||||
@@ -562,7 +592,7 @@ func (a *App) getLinkMetadata(c request.CTX, requestURL string, timestamp int64,
|
||||
permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPost, referencedTeam, referencedChannel)}
|
||||
} else {
|
||||
// referencedPost does not contain a permalink: we get its metadata
|
||||
referencedPostWithMetadata := a.PreparePostForClientWithEmbedsAndImages(c, referencedPost, false, false)
|
||||
referencedPostWithMetadata := a.PreparePostForClientWithEmbedsAndImages(c, referencedPost, false, false, false)
|
||||
permalink = &model.Permalink{PreviewPost: model.NewPreviewPost(referencedPostWithMetadata, referencedTeam, referencedChannel)}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
Message: message,
|
||||
}
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, true)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, true, false)
|
||||
|
||||
t.Run("doesn't mutate provided post", func(t *testing.T) {
|
||||
assert.NotEqual(t, clientPost, post, "should've returned a new post")
|
||||
@@ -151,7 +151,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
|
||||
post := th.CreatePost(th.BasicChannel)
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
|
||||
assert.False(t, clientPost == post, "should've returned a new post")
|
||||
assert.Equal(t, clientPost, post, "shouldn't have changed any metadata")
|
||||
@@ -167,7 +167,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
reaction3 := th.AddReactionToPost(post, th.BasicUser2, "ice_cream")
|
||||
post.HasReactions = true
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
|
||||
assert.Len(t, clientPost.Metadata.Reactions, 3, "should've populated Reactions")
|
||||
assert.Equal(t, reaction1, clientPost.Metadata.Reactions[0], "first reaction is incorrect")
|
||||
@@ -194,7 +194,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
|
||||
var clientPost *model.Post
|
||||
assert.Eventually(t, func() bool {
|
||||
clientPost = th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost = th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
return assert.ObjectsAreEqual([]*model.FileInfo{fileInfo}, clientPost.Metadata.Files)
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
@@ -230,7 +230,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
th.AddReactionToPost(post, th.BasicUser2, "angry")
|
||||
post.HasReactions = true
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
|
||||
t.Run("populates emojis", func(t *testing.T) {
|
||||
assert.ElementsMatch(t, []*model.Emoji{}, clientPost.Metadata.Emojis, "should've populated empty Emojis")
|
||||
@@ -275,7 +275,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
th.AddReactionToPost(post, th.BasicUser2, "angry")
|
||||
post.HasReactions = true
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
|
||||
t.Run("populates emojis", func(t *testing.T) {
|
||||
assert.ElementsMatch(t, []*model.Emoji{emoji1, emoji2, emoji3, emoji4}, clientPost.Metadata.Emojis, "should've populated post.Emojis")
|
||||
@@ -307,7 +307,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
post.AddProp(model.PostPropsOverrideIconURL, url)
|
||||
post.AddProp(model.PostPropsOverrideIconEmoji, emoji)
|
||||
|
||||
return th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
return th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
}
|
||||
|
||||
emoji := "basketball"
|
||||
@@ -361,7 +361,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
|
||||
t.Run("populates image dimensions", func(t *testing.T) {
|
||||
imageDimensions := clientPost.Metadata.Images
|
||||
@@ -394,7 +394,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
post.AddProp(model.PostPropsOverrideIconEmoji, true)
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
_ = th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
_ = th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -424,7 +424,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
post.Metadata.Embeds = nil
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false, false)
|
||||
|
||||
// Reminder that only the first link gets an embed and dimensions
|
||||
|
||||
@@ -459,7 +459,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
firstEmbed := clientPost.Metadata.Embeds[0]
|
||||
ogData := firstEmbed.Data.(*opengraph.OpenGraph)
|
||||
|
||||
@@ -502,7 +502,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
post.Metadata.Embeds = nil
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false, false)
|
||||
|
||||
t.Run("populates embeds", func(t *testing.T) {
|
||||
assert.ElementsMatch(t, []*model.PostEmbed{
|
||||
@@ -547,7 +547,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
// DeleteAt isn't set on the post returned by App.DeletePost
|
||||
post.DeleteAt = model.GetMillis()
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
|
||||
assert.NotEqual(t, nil, clientPost.Metadata, "should've populated Metadata“")
|
||||
assert.Equal(t, "", clientPost.Message, "should've cleaned post content")
|
||||
@@ -582,7 +582,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
previewPost.Metadata.Embeds = nil
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false)
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false)
|
||||
firstEmbed := clientPost.Metadata.Embeds[0]
|
||||
preview := firstEmbed.Data.(*model.PreviewPost)
|
||||
require.Equal(t, referencedPost.Id, preview.PostID)
|
||||
@@ -641,7 +641,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
previewPost.Metadata.Embeds = nil
|
||||
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false)
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false)
|
||||
firstEmbed := clientPost.Metadata.Embeds[0]
|
||||
preview := firstEmbed.Data.(*model.PreviewPost)
|
||||
|
||||
@@ -679,7 +679,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
previewPost.Metadata.Embeds = nil
|
||||
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false)
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false)
|
||||
firstEmbed := clientPost.Metadata.Embeds[0]
|
||||
preview := firstEmbed.Data.(*model.PreviewPost)
|
||||
referencedPostFirstEmbed := preview.Post.Metadata.Embeds[0]
|
||||
@@ -726,7 +726,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
previewPost.Metadata.Embeds = nil
|
||||
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false)
|
||||
clientPost := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, previewPost, false, false, false)
|
||||
firstEmbed := clientPost.Metadata.Embeds[0]
|
||||
preview := firstEmbed.Data.(*model.PreviewPost)
|
||||
referencedPostMetadata := preview.Post.Metadata
|
||||
@@ -761,7 +761,7 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
}, th.BasicChannel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, previewPost, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, previewPost, false, false, false)
|
||||
firstEmbed := clientPost.Metadata.Embeds[0]
|
||||
preview := firstEmbed.Data.(*model.PreviewPost)
|
||||
require.Equal(t, referencedPost.Id, preview.PostID)
|
||||
@@ -770,13 +770,13 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
*cfg.ServiceSettings.EnablePermalinkPreviews = false
|
||||
})
|
||||
|
||||
th.App.PreparePostForClient(th.Context, previewPost, false, false)
|
||||
th.App.PreparePostForClient(th.Context, previewPost, false, false, false)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnablePermalinkPreviews = true
|
||||
})
|
||||
|
||||
clientPost2 := th.App.PreparePostForClient(th.Context, previewPost, false, false)
|
||||
clientPost2 := th.App.PreparePostForClient(th.Context, previewPost, false, false, false)
|
||||
firstEmbed2 := clientPost2.Metadata.Embeds[0]
|
||||
preview2 := firstEmbed2.Data.(*model.PreviewPost)
|
||||
require.Equal(t, referencedPost.Id, preview2.PostID)
|
||||
@@ -828,7 +828,7 @@ func testProxyLinkedImage(t *testing.T, th *TestHelper, shouldProxy bool) {
|
||||
Message: fmt.Sprintf(postTemplate, imageURL),
|
||||
}
|
||||
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false)
|
||||
clientPost := th.App.PreparePostForClient(th.Context, post, false, false, false)
|
||||
|
||||
if shouldProxy {
|
||||
assert.Equal(t, fmt.Sprintf(postTemplate, imageURL), post.Message, "should not have mutated original post")
|
||||
@@ -876,7 +876,7 @@ func testProxyOpenGraphImage(t *testing.T, th *TestHelper, shouldProxy bool) {
|
||||
require.Nil(t, err)
|
||||
|
||||
post.Metadata.Embeds = nil
|
||||
embeds := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false).Metadata.Embeds
|
||||
embeds := th.App.PreparePostForClientWithEmbedsAndImages(th.Context, post, false, false, false).Metadata.Embeds
|
||||
require.Len(t, embeds, 1, "should have one embed")
|
||||
|
||||
embed := embeds[0]
|
||||
|
||||
34
app/post_priority.go
Обычный файл
34
app/post_priority.go
Обычный файл
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (a *App) GetPriorityForPost(postId string) (*model.PostPriority, *model.AppError) {
|
||||
priority, err := a.Srv().Store().PostPriority().GetForPost(postId)
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return priority, nil
|
||||
}
|
||||
|
||||
func (a *App) GetPriorityForPostList(list *model.PostList) (map[string]*model.PostPriority, *model.AppError) {
|
||||
priority, err := a.Srv().Store().PostPriority().GetForPosts(list.Order)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPriorityForPost", "app.post_prority.get_for_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
priorityMap := make(map[string]*model.PostPriority)
|
||||
for _, p := range priority {
|
||||
priorityMap[p.PostId] = p
|
||||
}
|
||||
|
||||
return priorityMap, nil
|
||||
}
|
||||
@@ -1598,7 +1598,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
@@ -1637,7 +1637,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post1 and post3 should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
@@ -1676,7 +1676,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post2 and post3 should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
@@ -1713,7 +1713,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
@@ -1755,7 +1755,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
@@ -1809,7 +1809,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post2 should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
@@ -1863,7 +1863,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post2 and post5 should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
@@ -1912,7 +1912,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// should be mentioned by post2 and post3
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
@@ -1942,12 +1942,12 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
|
||||
count, _, err = th.App.countMentionsFromPost(th.Context, user1, post1)
|
||||
count, _, _, err = th.App.countMentionsFromPost(th.Context, user1, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, count)
|
||||
@@ -1984,7 +1984,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post1 and post3 should mention the user, but we only count post3
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post2)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post2)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
@@ -2015,7 +2015,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post2 should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
@@ -2062,7 +2062,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post4 should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post3)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post3)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
@@ -2102,7 +2102,7 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// post3 should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
@@ -2138,11 +2138,70 @@ func TestCountMentionsFromPost(t *testing.T) {
|
||||
|
||||
// Every post should mention the user
|
||||
|
||||
count, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
count, _, _, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, numPosts, count)
|
||||
})
|
||||
|
||||
t.Run("should count urgent mentions", func(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.PostPriority = true
|
||||
cfg.FeatureFlags.PostPriority = true
|
||||
})
|
||||
|
||||
user1 := th.BasicUser
|
||||
user2 := th.BasicUser2
|
||||
|
||||
channel := th.CreateChannel(th.Context, th.BasicTeam)
|
||||
th.AddUserToChannel(user2, channel)
|
||||
|
||||
user2.NotifyProps[model.MentionKeysNotifyProp] = "apple"
|
||||
|
||||
post1, err := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: user1.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: fmt.Sprintf("@%s", user2.Username),
|
||||
Metadata: &model.PostMetadata{
|
||||
Priority: &model.PostPriority{
|
||||
Priority: model.NewString(model.PostPriorityUrgent),
|
||||
},
|
||||
},
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, err = th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: user1.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: fmt.Sprintf("@%s", user2.Username),
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, err = th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: user1.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "apple",
|
||||
Metadata: &model.PostMetadata{
|
||||
Priority: &model.PostPriority{
|
||||
Priority: model.NewString(model.PostPriorityUrgent),
|
||||
},
|
||||
},
|
||||
}, channel, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
// all posts mention the user but only post1, post3 are urgent
|
||||
|
||||
_, _, count, err := th.App.countMentionsFromPost(th.Context, user2, post1)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFillInPostProps(t *testing.T) {
|
||||
@@ -2887,7 +2946,7 @@ func TestComputeLastAccessiblePostTime(t *testing.T) {
|
||||
mockSystemStore.AssertCalled(t, "SaveOrUpdate", mock.Anything)
|
||||
})
|
||||
|
||||
t.Run("Do NOT update the time, if cloud limit is NOT applicable", func(t *testing.T) {
|
||||
t.Run("Remove the time if cloud limit is NOT applicable", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -2901,13 +2960,15 @@ func TestComputeLastAccessiblePostTime(t *testing.T) {
|
||||
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockSystemStore := storemocks.SystemStore{}
|
||||
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
|
||||
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessiblePostTime, Value: "10"}, nil)
|
||||
mockSystemStore.On("PermanentDeleteByName", mock.Anything).Return(nil, nil)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
|
||||
err := th.App.ComputeLastAccessiblePostTime()
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockSystemStore.AssertNotCalled(t, "SaveOrUpdate", mock.Anything)
|
||||
mockSystemStore.AssertCalled(t, "PermanentDeleteByName", mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -43,15 +43,13 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction)
|
||||
// The post is always modified since the UpdateAt always changes
|
||||
a.invalidateCacheForChannelPosts(post.ChannelId)
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ReactionHasBeenAdded(pluginContext, reaction)
|
||||
return true
|
||||
}, plugin.ReactionHasBeenAddedID)
|
||||
})
|
||||
}
|
||||
pluginContext := pluginContext(c)
|
||||
a.Srv().Go(func() {
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ReactionHasBeenAdded(pluginContext, reaction)
|
||||
return true
|
||||
}, plugin.ReactionHasBeenAddedID)
|
||||
})
|
||||
|
||||
a.Srv().Go(func() {
|
||||
a.sendReactionEvent(model.WebsocketEventReactionAdded, reaction, post)
|
||||
@@ -142,15 +140,13 @@ func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction
|
||||
// The post is always modified since the UpdateAt always changes
|
||||
a.invalidateCacheForChannelPosts(post.ChannelId)
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ReactionHasBeenRemoved(pluginContext, reaction)
|
||||
return true
|
||||
}, plugin.ReactionHasBeenRemovedID)
|
||||
})
|
||||
}
|
||||
pluginContext := pluginContext(c)
|
||||
a.Srv().Go(func() {
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ReactionHasBeenRemoved(pluginContext, reaction)
|
||||
return true
|
||||
}, plugin.ReactionHasBeenRemovedID)
|
||||
})
|
||||
|
||||
a.Srv().Go(func() {
|
||||
a.sendReactionEvent(model.WebsocketEventReactionRemoved, reaction, post)
|
||||
|
||||
@@ -161,6 +161,8 @@ type Server struct {
|
||||
tracer *tracing.Tracer
|
||||
|
||||
products map[string]Product
|
||||
|
||||
hooksManager *product.HooksManager
|
||||
}
|
||||
|
||||
func (s *Server) Store() store.Store {
|
||||
@@ -255,6 +257,8 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
return nil, errors.Wrapf(err, "unable to create teams service")
|
||||
}
|
||||
|
||||
s.hooksManager = product.NewHooksManager(s.GetMetrics())
|
||||
|
||||
// ensure app implements `product.UserService`
|
||||
var _ product.UserService = (*App)(nil)
|
||||
|
||||
@@ -283,7 +287,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
// It is important to initialize the hub only after the global logger is set
|
||||
// to avoid race conditions while logging from inside the hub.
|
||||
// Step 5: Start hub in platform which the hub depends on s.Channels() (step 4)
|
||||
s.platform.Start(New(ServerConnector(s.Channels())))
|
||||
s.platform.Start()
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Everything below this is not order sensitive and safe to be moved around.
|
||||
@@ -365,7 +369,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
})
|
||||
s.htmlTemplateWatcher = htmlTemplateWatcher
|
||||
|
||||
s.telemetryService = telemetry.New(New(ServerConnector(s.Channels())), s.Store(), s.platform.SearchEngine, s.Log())
|
||||
s.telemetryService = telemetry.New(New(ServerConnector(s.Channels())), s.Store(), s.platform.SearchEngine, s.Log(), *s.Config().LogSettings.VerboseDiagnostics)
|
||||
s.platform.SetTelemetryId(s.TelemetryId()) // TODO: move this into platform once telemetry service moved to platform.
|
||||
|
||||
emailService, err := email.NewService(email.ServiceConfig{
|
||||
|
||||
@@ -235,23 +235,6 @@ func (a *App) AttachDeviceId(sessionID string, deviceID string, expiresAt int64)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateLastActivityAtIfNeeded(session model.Session) {
|
||||
now := model.GetMillis()
|
||||
|
||||
a.UpdateWebConnUserActivity(session, now)
|
||||
|
||||
if now-session.LastActivityAt < model.SessionActivityTimeout {
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.Srv().Store().Session().UpdateLastActivityAt(session.Id, now); err != nil {
|
||||
mlog.Warn("Failed to update LastActivityAt", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id), mlog.Err(err))
|
||||
}
|
||||
|
||||
session.LastActivityAt = now
|
||||
a.ch.srv.platform.AddSessionToCache(&session)
|
||||
}
|
||||
|
||||
// ExtendSessionExpiryIfNeeded extends Session.ExpiresAt based on session lengths in config.
|
||||
// A new ExpiresAt is only written if enough time has elapsed since last update.
|
||||
// Returns true only if the session was extended.
|
||||
|
||||
159
app/status.go
159
app/status.go
@@ -21,174 +21,33 @@ func (a *App) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.Ap
|
||||
// status to away if needed. Used by the WS to set status to away if an 'online' device disconnects
|
||||
// while an 'away' device is still connected
|
||||
func (a *App) SetStatusLastActivityAt(userID string, activityAt int64) {
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
if status, err = a.GetStatus(userID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
status.LastActivityAt = activityAt
|
||||
|
||||
a.Srv().Platform().AddStatusCacheSkipClusterSend(status)
|
||||
a.SetStatusAwayIfNeeded(userID, false)
|
||||
a.Srv().Platform().SetStatusLastActivityAt(userID, activityAt)
|
||||
}
|
||||
|
||||
func (a *App) SetStatusOnline(userID string, manual bool) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
broadcast := false
|
||||
|
||||
var oldStatus string = model.StatusOffline
|
||||
var oldTime int64
|
||||
var oldManual bool
|
||||
var status *model.Status
|
||||
var err *model.AppError
|
||||
|
||||
if status, err = a.GetStatus(userID); err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
|
||||
broadcast = true
|
||||
} else {
|
||||
if status.Manual && !manual {
|
||||
return // manually set status always overrides non-manual one
|
||||
}
|
||||
|
||||
if status.Status != model.StatusOnline {
|
||||
broadcast = true
|
||||
}
|
||||
|
||||
oldStatus = status.Status
|
||||
oldTime = status.LastActivityAt
|
||||
oldManual = status.Manual
|
||||
|
||||
status.Status = model.StatusOnline
|
||||
status.Manual = false // for "online" there's no manual setting
|
||||
status.LastActivityAt = model.GetMillis()
|
||||
}
|
||||
|
||||
a.Srv().Platform().AddStatusCache(status)
|
||||
|
||||
// Only update the database if the status has changed, the status has been manually set,
|
||||
// or enough time has passed since the previous action
|
||||
if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.StatusMinUpdateTime {
|
||||
if broadcast {
|
||||
if err := a.Srv().Store().Status().SaveOrUpdate(status); err != nil {
|
||||
mlog.Warn("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
|
||||
}
|
||||
} else {
|
||||
if err := a.Srv().Store().Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt); err != nil {
|
||||
mlog.Error("Failed to save status", mlog.String("user_id", userID), mlog.Err(err), mlog.String("user_id", userID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if broadcast {
|
||||
a.Srv().Platform().BroadcastStatus(status)
|
||||
}
|
||||
a.Srv().Platform().SetStatusOnline(userID, manual)
|
||||
}
|
||||
|
||||
func (a *App) SetStatusOffline(userID string, manual bool) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := a.GetStatus(userID)
|
||||
if err == nil && status.Manual && !manual {
|
||||
return // manually set status always overrides non-manual one
|
||||
}
|
||||
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: model.GetMillis(), ActiveChannel: ""}
|
||||
|
||||
a.Srv().Platform().SaveAndBroadcastStatus(status)
|
||||
a.Srv().Platform().SetStatusOffline(userID, manual)
|
||||
}
|
||||
|
||||
func (a *App) SetStatusAwayIfNeeded(userID string, manual bool) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := a.GetStatus(userID)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: manual, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
if !manual && status.Manual {
|
||||
return // manually set status always overrides non-manual one
|
||||
}
|
||||
|
||||
if !manual {
|
||||
if status.Status == model.StatusAway {
|
||||
return
|
||||
}
|
||||
|
||||
if !a.IsUserAway(status.LastActivityAt) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
status.Status = model.StatusAway
|
||||
status.Manual = manual
|
||||
status.ActiveChannel = ""
|
||||
|
||||
a.Srv().Platform().SaveAndBroadcastStatus(status)
|
||||
a.Srv().Platform().SetStatusAwayIfNeeded(userID, manual)
|
||||
}
|
||||
|
||||
// SetStatusDoNotDisturbTimed takes endtime in unix epoch format in UTC
|
||||
// and sets status of given userId to dnd which will be restored back after endtime
|
||||
func (a *App) SetStatusDoNotDisturbTimed(userId string, endtime int64) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := a.GetStatus(userId)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userId, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
status.PrevStatus = status.Status
|
||||
status.Status = model.StatusDnd
|
||||
status.Manual = true
|
||||
|
||||
status.DNDEndTime = endtime
|
||||
|
||||
a.Srv().Platform().SaveAndBroadcastStatus(status)
|
||||
a.Srv().Platform().SetStatusDoNotDisturbTimed(userId, endtime)
|
||||
}
|
||||
|
||||
func (a *App) SetStatusDoNotDisturb(userID string) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := a.GetStatus(userID)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
status.Status = model.StatusDnd
|
||||
status.Manual = true
|
||||
|
||||
a.Srv().Platform().SaveAndBroadcastStatus(status)
|
||||
a.Srv().Platform().SetStatusDoNotDisturb(userID)
|
||||
}
|
||||
|
||||
func (a *App) SetStatusOutOfOffice(userID string) {
|
||||
if !*a.Config().ServiceSettings.EnableUserStatuses {
|
||||
return
|
||||
}
|
||||
|
||||
status, err := a.GetStatus(userID)
|
||||
|
||||
if err != nil {
|
||||
status = &model.Status{UserId: userID, Status: model.StatusOutOfOffice, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
|
||||
}
|
||||
|
||||
status.Status = model.StatusOutOfOffice
|
||||
status.Manual = true
|
||||
|
||||
a.Srv().Platform().SaveAndBroadcastStatus(status)
|
||||
a.Srv().Platform().SetStatusOutOfOffice(userID)
|
||||
}
|
||||
|
||||
func (a *App) GetStatusFromCache(userID string) *model.Status {
|
||||
@@ -199,10 +58,6 @@ func (a *App) GetStatus(userID string) (*model.Status, *model.AppError) {
|
||||
return a.Srv().Platform().GetStatus(userID)
|
||||
}
|
||||
|
||||
func (a *App) IsUserAway(lastActivityAt int64) bool {
|
||||
return model.GetMillis()-lastActivityAt >= *a.Config().TeamSettings.UserStatusAwayTimeout*1000
|
||||
}
|
||||
|
||||
// UpdateDNDStatusOfUsers is a recurring task which is started when server starts
|
||||
// which unsets dnd status of users if needed and saves and broadcasts it
|
||||
func (a *App) UpdateDNDStatusOfUsers() {
|
||||
|
||||
68
app/team.go
68
app/team.go
@@ -308,6 +308,8 @@ func (a *App) UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError)
|
||||
return nil, model.NewAppError("UpdateTeamScheme", "app.team.clear_cache.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
a.Srv().Store().Channel().ClearMembersForUserCache()
|
||||
|
||||
if appErr := a.sendTeamEvent(oldTeam, model.WebsocketEventUpdateTeamScheme); appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
@@ -844,21 +846,19 @@ func (a *App) JoinUserToTeam(c request.CTX, team *model.Team, user *model.User,
|
||||
a.InvalidateCacheForUser(user.Id)
|
||||
a.invalidateCacheForUserTeams(user.Id)
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var actor *model.User
|
||||
if userRequestorId != "" {
|
||||
actor, _ = a.GetUser(userRequestorId)
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedTeam(pluginContext, teamMember, actor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedTeamID)
|
||||
})
|
||||
var actor *model.User
|
||||
if userRequestorId != "" {
|
||||
actor, _ = a.GetUser(userRequestorId)
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedTeam(pluginContext, teamMember, actor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedTeamID)
|
||||
})
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventAddedToTeam, "", "", user.Id, nil, "")
|
||||
message.Add("team_id", team.Id)
|
||||
message.Add("user_id", user.Id)
|
||||
@@ -1218,21 +1218,19 @@ func (a *App) RemoveUserFromTeam(c request.CTX, teamID string, userID string, re
|
||||
}
|
||||
|
||||
func (a *App) postProcessTeamMemberLeave(c request.CTX, teamMember *model.TeamMember, requestorId string) *model.AppError {
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
var actor *model.User
|
||||
if requestorId != "" {
|
||||
actor, _ = a.GetUser(requestorId)
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftTeam(pluginContext, teamMember, actor)
|
||||
return true
|
||||
}, plugin.UserHasLeftTeamID)
|
||||
})
|
||||
var actor *model.User
|
||||
if requestorId != "" {
|
||||
actor, _ = a.GetUser(requestorId)
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftTeam(pluginContext, teamMember, actor)
|
||||
return true
|
||||
}, plugin.UserHasLeftTeamID)
|
||||
})
|
||||
|
||||
user, nErr := a.Srv().Store().User().Get(context.Background(), teamMember.UserId)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
@@ -1741,13 +1739,14 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include
|
||||
} else {
|
||||
teamIDs = append(teamIDs, id)
|
||||
membersMap[id] = unreads(data[i], &model.TeamUnread{
|
||||
MsgCount: 0,
|
||||
MentionCount: 0,
|
||||
MentionCountRoot: 0,
|
||||
MsgCountRoot: 0,
|
||||
ThreadCount: 0,
|
||||
ThreadMentionCount: 0,
|
||||
TeamId: id,
|
||||
MsgCount: 0,
|
||||
MentionCount: 0,
|
||||
MentionCountRoot: 0,
|
||||
MsgCountRoot: 0,
|
||||
ThreadCount: 0,
|
||||
ThreadMentionCount: 0,
|
||||
ThreadUrgentMentionCount: 0,
|
||||
TeamId: id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1755,7 +1754,7 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include
|
||||
includeCollapsedThreads = includeCollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
|
||||
|
||||
if includeCollapsedThreads {
|
||||
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs)
|
||||
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.isPostPriorityEnabled())
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
@@ -1763,6 +1762,7 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include
|
||||
if _, ok := teamUnreads[teamID]; ok {
|
||||
member.ThreadCount = teamUnreads[teamID].ThreadCount
|
||||
member.ThreadMentionCount = teamUnreads[teamID].ThreadMentionCount
|
||||
member.ThreadUrgentMentionCount = teamUnreads[teamID].ThreadUrgentMentionCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ func TestAddUserToTeam(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("block user by domain but allow bot", func(t *testing.T) {
|
||||
t.Skip("MM-48973")
|
||||
th.BasicTeam.AllowedDomains = "example.com"
|
||||
_, err := th.App.UpdateTeam(th.BasicTeam)
|
||||
require.Nil(t, err, "Should update the team")
|
||||
@@ -1109,6 +1110,45 @@ func TestAppUpdateTeamScheme(t *testing.T) {
|
||||
updatedTeam, err := th.App.UpdateTeamScheme(th.BasicTeam)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, mockID, updatedTeam.SchemeId, "Wrong Team SchemeId")
|
||||
|
||||
// Test that a newly applied team scheme applies the new permissions to a team member
|
||||
th.App.SetPhase2PermissionsMigrationStatus(true)
|
||||
|
||||
team2Scheme := th.SetupTeamScheme()
|
||||
channelUser, err := th.App.GetRoleByName(context.Background(), team2Scheme.DefaultChannelUserRole)
|
||||
require.Nil(t, err)
|
||||
channelUser.Permissions = []string{}
|
||||
_, err = th.App.UpdateRole(channelUser) // Remove all permissions from the team user role of the scheme
|
||||
require.Nil(t, err)
|
||||
|
||||
channelAdmin, err := th.App.GetRoleByName(context.Background(), team2Scheme.DefaultChannelAdminRole)
|
||||
require.Nil(t, err)
|
||||
channelAdmin.Permissions = []string{}
|
||||
_, err = th.App.UpdateRole(channelAdmin) // Remove all permissions from the team admin role of the scheme
|
||||
require.Nil(t, err)
|
||||
|
||||
team2 := th.CreateTeam()
|
||||
th.App.AddUserToTeam(th.Context, team2.Id, th.BasicUser.Id, "")
|
||||
channel := th.CreateChannel(th.Context, team2)
|
||||
th.App.AddUserToChannel(th.Context, th.BasicUser, channel, true)
|
||||
session := model.Session{
|
||||
Roles: model.SystemUserRoleId,
|
||||
UserId: th.BasicUser.Id,
|
||||
TeamMembers: []*model.TeamMember{
|
||||
{
|
||||
UserId: th.BasicUser.Id,
|
||||
TeamId: team2.Id,
|
||||
SchemeUser: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
// ensure user can update channel properties before applying the scheme
|
||||
require.True(t, th.App.SessionHasPermissionToChannel(th.Context, session, channel.Id, model.PermissionManagePublicChannelProperties))
|
||||
// apply the team scheme
|
||||
team2.SchemeId = &team2Scheme.Id
|
||||
_, err = th.App.UpdateTeamScheme(team2)
|
||||
require.Nil(t, err)
|
||||
require.False(t, th.App.SessionHasPermissionToChannel(th.Context, session, channel.Id, model.PermissionManagePublicChannelProperties))
|
||||
}
|
||||
|
||||
func TestGetTeamMembers(t *testing.T) {
|
||||
|
||||
@@ -43,7 +43,6 @@ func (ts *TeamService) GetTeams(teamIDs []string) ([]*model.Team, error) {
|
||||
}
|
||||
|
||||
// CreateDefaultChannels creates channels in the given team for each channel returned by (*App).DefaultChannelNames.
|
||||
//
|
||||
func (ts *TeamService) createDefaultChannels(teamID string) ([]*model.Channel, error) {
|
||||
displayNames := map[string]string{
|
||||
"town-square": i18n.T("api.channel.create_default_channels.town_square"),
|
||||
@@ -192,11 +191,25 @@ func (ts *TeamService) JoinUserToTeam(team *model.Team, user *model.User) (*mode
|
||||
// RemoveTeamMember removes the team member from the team. This method sends
|
||||
// the websocket message before actually removing so the user being removed gets it.
|
||||
func (ts *TeamService) RemoveTeamMember(teamMember *model.TeamMember) error {
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, teamMember.TeamId, "", "", nil, "")
|
||||
message.Add("user_id", teamMember.UserId)
|
||||
message.Add("team_id", teamMember.TeamId)
|
||||
ts.wh.Publish(message)
|
||||
/*
|
||||
MM-43850: send leave_team event to user using `ReliableClusterSend` to improve safety
|
||||
*/
|
||||
// message for other team members
|
||||
omitUsers := make(map[string]bool, 1)
|
||||
omitUsers[teamMember.UserId] = true
|
||||
messageTeam := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, teamMember.TeamId, "", "", omitUsers, "")
|
||||
messageTeam.Add("user_id", teamMember.UserId)
|
||||
messageTeam.Add("team_id", teamMember.TeamId)
|
||||
ts.wh.Publish(messageTeam)
|
||||
|
||||
// message for teamMember.UserId
|
||||
messageUser := model.NewWebSocketEvent(model.WebsocketEventLeaveTeam, "", "", teamMember.UserId, nil, "")
|
||||
messageUser.Add("user_id", teamMember.UserId)
|
||||
messageUser.Add("team_id", teamMember.TeamId)
|
||||
|
||||
ts.wh.Publish(messageUser)
|
||||
|
||||
// delete team member
|
||||
teamMember.Roles = ""
|
||||
teamMember.DeleteAt = model.GetMillis()
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
)
|
||||
|
||||
// By default the list will be (not necessarily in this order):
|
||||
//
|
||||
// ['town-square', 'off-topic']
|
||||
//
|
||||
// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace
|
||||
// 'off-topic' and be included in the return results in addition to 'town-square'. For example:
|
||||
// ['town-square', 'game-of-thrones', 'wow']
|
||||
//
|
||||
// ['town-square', 'game-of-thrones', 'wow']
|
||||
func (ts *TeamService) DefaultChannelNames() []string {
|
||||
names := []string{"town-square"}
|
||||
|
||||
|
||||
@@ -49,11 +49,6 @@ func (a *App) genFileInfoFromReader(name string, file io.ReadSeeker, size int64)
|
||||
}
|
||||
|
||||
func (a *App) runPluginsHook(c *request.Context, info *model.FileInfo, file io.Reader) *model.AppError {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
filePath := info.Path
|
||||
// using a pipe to avoid loading the whole file content in memory.
|
||||
r, w := io.Pipe()
|
||||
@@ -67,7 +62,7 @@ func (a *App) runPluginsHook(c *request.Context, info *model.FileInfo, file io.R
|
||||
var rejErr *model.AppError
|
||||
var once sync.Once
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
once.Do(func() {
|
||||
hookHasRunCh <- struct{}{}
|
||||
})
|
||||
@@ -127,11 +122,6 @@ func (a *App) runPluginsHook(c *request.Context, info *model.FileInfo, file io.R
|
||||
}
|
||||
|
||||
func (a *App) CreateUploadSession(c request.CTX, us *model.UploadSession) (*model.UploadSession, *model.AppError) {
|
||||
if us.FileSize > *a.Config().FileSettings.MaxFileSize {
|
||||
return nil, model.NewAppError("CreateUploadSession", "app.upload.create.upload_too_large.app_error",
|
||||
map[string]any{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
|
||||
us.FileOffset = 0
|
||||
now := time.Now()
|
||||
us.CreateAt = model.GetMillisForTime(now)
|
||||
|
||||
@@ -32,16 +32,6 @@ func TestCreateUploadSession(t *testing.T) {
|
||||
FileSize: 8 * 1024 * 1024,
|
||||
}
|
||||
|
||||
t.Run("FileSize over limit", func(t *testing.T) {
|
||||
maxFileSize := *th.App.Config().FileSettings.MaxFileSize
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = us.FileSize - 1 })
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize })
|
||||
u, err := th.App.CreateUploadSession(th.Context, us)
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "app.upload.create.upload_too_large.app_error", err.Id)
|
||||
require.Nil(t, u)
|
||||
})
|
||||
|
||||
t.Run("invalid Id", func(t *testing.T) {
|
||||
u, err := th.App.CreateUploadSession(th.Context, us)
|
||||
require.NotNil(t, err)
|
||||
|
||||
31
app/usage.go
31
app/usage.go
@@ -10,37 +10,6 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/utils"
|
||||
)
|
||||
|
||||
// CheckFreemiumLimitsForConfigSave returns an error if the configuration being saved violates a cloud plan's limits
|
||||
func (a *App) CheckFreemiumLimitsForConfigSave(oldConfig, newConfig *model.Config) *model.AppError {
|
||||
appErr := a.checkIntegrationLimitsForConfigSave(oldConfig, newConfig)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIntegrationsUsage returns usage information on enabled integrations
|
||||
func (a *App) GetIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError) {
|
||||
return a.ch.getIntegrationsUsage()
|
||||
}
|
||||
|
||||
func (ch *Channels) getIntegrationsUsage() (*model.IntegrationsUsage, *model.AppError) {
|
||||
installed, appErr := ch.getInstalledIntegrations()
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
var count = 0
|
||||
for _, i := range installed {
|
||||
if i.Enabled {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return &model.IntegrationsUsage{Enabled: count}, nil
|
||||
}
|
||||
|
||||
// GetPostsUsage returns the total posts count rounded down to the most
|
||||
// significant digit
|
||||
func (a *App) GetPostsUsage() (int64, *model.AppError) {
|
||||
|
||||
44
app/user.go
44
app/user.go
@@ -308,15 +308,21 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
|
||||
message.Add("user_id", ruser.Id)
|
||||
a.Publish(message)
|
||||
|
||||
if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||
a.Srv().Go(func() {
|
||||
pluginContext := pluginContext(c)
|
||||
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasBeenCreated(pluginContext, ruser)
|
||||
return true
|
||||
}, plugin.UserHasBeenCreatedID)
|
||||
})
|
||||
}
|
||||
pluginContext := pluginContext(c)
|
||||
a.Srv().Go(func() {
|
||||
a.ch.RunMultiHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasBeenCreated(pluginContext, ruser)
|
||||
return true
|
||||
}, plugin.UserHasBeenCreatedID)
|
||||
})
|
||||
|
||||
// Create/Update the subscriptionHistoryEvent
|
||||
go func() {
|
||||
_, err := a.SendSubscriptionHistoryEvent(ruser.Id)
|
||||
if err != nil {
|
||||
c.Logger().Error("Failed to create/update the SubscriptionHistoryEvent", mlog.Err(err))
|
||||
}
|
||||
}()
|
||||
|
||||
return ruser, nil
|
||||
}
|
||||
@@ -2391,6 +2397,10 @@ func (a *App) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.U
|
||||
func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
|
||||
var result model.Threads
|
||||
var eg errgroup.Group
|
||||
postPriorityIsEnabled := a.isPostPriorityEnabled()
|
||||
if postPriorityIsEnabled {
|
||||
options.IncludeIsUrgent = true
|
||||
}
|
||||
|
||||
if !options.ThreadsOnly {
|
||||
eg.Go(func() error {
|
||||
@@ -2427,6 +2437,18 @@ func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThre
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if postPriorityIsEnabled {
|
||||
eg.Go(func() error {
|
||||
totalUnreadUrgentMentions, err := a.Srv().Store().Thread().GetTotalUnreadUrgentMentions(userID, teamID, options)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to count urgent mentioned threads for user id=%s", userID)
|
||||
}
|
||||
result.TotalUnreadUrgentMentions = totalUnreadUrgentMentions
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !options.TotalsOnly {
|
||||
@@ -2469,7 +2491,7 @@ func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.Thread
|
||||
}
|
||||
|
||||
func (a *App) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
|
||||
thread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended)
|
||||
thread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled())
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetThreadForUser", "app.user.get_threads_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
@@ -2551,7 +2573,7 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, tea
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
|
||||
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true)
|
||||
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.isPostPriorityEnabled())
|
||||
|
||||
if err != nil {
|
||||
var errNotFound *store.ErrNotFound
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
oauthgitlab "github.com/mattermost/mattermost-server/v6/model/gitlab"
|
||||
oauthgitlab "github.com/mattermost/mattermost-server/v6/model/oauthproviders/gitlab"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
storemocks "github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/utils/testutils"
|
||||
@@ -1852,3 +1852,85 @@ func TestIsFirstAdmin(t *testing.T) {
|
||||
require.True(t, isFirstAdmin)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendSubscriptionHistoryEvent(t *testing.T) {
|
||||
cloudProduct := &model.Product{
|
||||
ID: "prod_test1",
|
||||
Name: "name1",
|
||||
Description: "description1",
|
||||
PricePerSeat: 1000,
|
||||
SKU: "sku1",
|
||||
PriceID: "price_id1",
|
||||
Family: "family1",
|
||||
RecurringInterval: "year",
|
||||
BillingScheme: "billing_scheme1",
|
||||
CrossSellsTo: "prod_test2",
|
||||
}
|
||||
|
||||
subscription := &model.Subscription{
|
||||
ID: "MySubscriptionID",
|
||||
CustomerID: "MyCustomer",
|
||||
ProductID: "SomeProductId",
|
||||
AddOns: []string{},
|
||||
StartAt: 1000000000,
|
||||
EndAt: 2000000000,
|
||||
CreateAt: 1000000000,
|
||||
Seats: 10,
|
||||
DNS: "some.dns.server",
|
||||
IsPaidTier: "false",
|
||||
}
|
||||
|
||||
subscriptionHistory := &model.SubscriptionHistory{
|
||||
ID: "sub_history",
|
||||
SubscriptionID: "MySubscriptionID",
|
||||
Seats: 10,
|
||||
CreateAt: 1000000000,
|
||||
}
|
||||
|
||||
t.Run("Should not create SubscriptionHistoryEvent if the license is not cloud", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense(""))
|
||||
|
||||
userID := "123"
|
||||
|
||||
subscriptionHistoryEvent, err := th.App.SendSubscriptionHistoryEvent(userID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, subscriptionHistoryEvent)
|
||||
})
|
||||
|
||||
t.Run("Should create SubscriptionHistoryEvent if the license is cloud and the product is yearly", func(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
cloud := mocks.CloudInterface{}
|
||||
|
||||
// mock the cloud functions
|
||||
cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil)
|
||||
cloud.Mock.On("GetCloudProduct", mock.Anything, mock.Anything).Return(cloudProduct, nil)
|
||||
cloud.Mock.On("CreateOrUpdateSubscriptionHistoryEvent", mock.Anything, mock.Anything).Return(subscriptionHistory, nil)
|
||||
|
||||
cloudImpl := th.App.Srv().Cloud
|
||||
defer func() {
|
||||
th.App.Srv().Cloud = cloudImpl
|
||||
}()
|
||||
th.App.Srv().Cloud = &cloud
|
||||
|
||||
// Mock to get the user count
|
||||
mockStore := th.App.Srv().Store().(*storemocks.Store)
|
||||
mockUserStore := storemocks.UserStore{}
|
||||
mockUserStore.On("Count", mock.Anything).Return(int64(10), nil)
|
||||
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
|
||||
userID := "123"
|
||||
|
||||
subscriptionHistoryEvent, err := th.App.SendSubscriptionHistoryEvent(userID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, subscription.ID, subscriptionHistoryEvent.SubscriptionID, "subscription ID doesn't match")
|
||||
require.Equal(t, 10, subscriptionHistoryEvent.Seats, "Number of seats doesn't match")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@ func (a *App) PopulateWebConnConfig(s *model.Session, cfg *platform.WebConnConfi
|
||||
|
||||
// NewWebConn returns a new WebConn instance.
|
||||
func (a *App) NewWebConn(cfg *platform.WebConnConfig) *platform.WebConn {
|
||||
return a.Srv().Platform().NewWebConn(cfg, a, a.ch.GetPluginsEnvironment)
|
||||
return a.Srv().Platform().NewWebConn(cfg, a, a.ch)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ func TestWebConnShouldSendEvent(t *testing.T) {
|
||||
{"should only send to non-admins", &model.WebsocketBroadcast{ContainsSanitizedData: true}, true, true, false, true},
|
||||
{"should send to nobody", &model.WebsocketBroadcast{ContainsSensitiveData: true, ContainsSanitizedData: true}, false, false, false, false},
|
||||
{"should omit basic user 2 by connection id", &model.WebsocketBroadcast{OmitConnectionId: user2ConnID}, true, false, true, true},
|
||||
{"should omit basic user 2 by connection id while user is set", &model.WebsocketBroadcast{UserId: th.BasicUser2.Id, OmitConnectionId: user2ConnID}, false, false, false, false},
|
||||
// needs more cases to get full coverage
|
||||
}
|
||||
|
||||
|
||||
52
app/worktemplates.go
Обычный файл
52
app/worktemplates.go
Обычный файл
@@ -0,0 +1,52 @@
|
||||
// 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/v6/app/worktemplates"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
)
|
||||
|
||||
func (a *App) GetWorkTemplateCategories(t i18n.TranslateFunc) ([]*model.WorkTemplateCategory, *model.AppError) {
|
||||
categories, err := worktemplates.ListCategories()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetWorkTemplateCategories", "app.worktemplates.get_categories.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
modelCategories := make([]*model.WorkTemplateCategory, len(categories))
|
||||
for i := range categories {
|
||||
modelCategories[i] = &model.WorkTemplateCategory{
|
||||
ID: categories[i].ID,
|
||||
Name: t(categories[i].Name),
|
||||
}
|
||||
}
|
||||
|
||||
return modelCategories, nil
|
||||
}
|
||||
|
||||
func (a *App) GetWorkTemplates(category string, featureFlags map[string]string, t i18n.TranslateFunc) ([]*model.WorkTemplate, *model.AppError) {
|
||||
templates, err := worktemplates.ListByCategory(category)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetWorkTemplates", "app.worktemplates.get_templates.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// filter out templates that are not enabled by feature Flag
|
||||
enabledTemplates := []*model.WorkTemplate{}
|
||||
for _, template := range templates {
|
||||
mTemplate := template.ToModelWorkTemplate(t)
|
||||
if template.FeatureFlag == nil {
|
||||
enabledTemplates = append(enabledTemplates, mTemplate)
|
||||
continue
|
||||
}
|
||||
|
||||
if featureFlags[template.FeatureFlag.Name] == template.FeatureFlag.Value {
|
||||
enabledTemplates = append(enabledTemplates, mTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
return enabledTemplates, nil
|
||||
}
|
||||
2
app/worktemplates/categories.yaml
Обычный файл
2
app/worktemplates/categories.yaml
Обычный файл
@@ -0,0 +1,2 @@
|
||||
- id: product_teams
|
||||
name: worktemplate.category.product_teams
|
||||
154
app/worktemplates/generator/main.go
Обычный файл
154
app/worktemplates/generator/main.go
Обычный файл
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/tools/imports"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type WorkTemplateWithMD5 struct {
|
||||
worktemplates.WorkTemplate
|
||||
MD5 string
|
||||
}
|
||||
|
||||
type WorkTemplateCategoryWithMD5 struct {
|
||||
worktemplates.WorkTemplateCategory `yaml:",inline"`
|
||||
MD5 string
|
||||
}
|
||||
|
||||
func getFileContent(filename string) ([]byte, error) {
|
||||
return os.ReadFile(path.Join(filename))
|
||||
}
|
||||
|
||||
func main() {
|
||||
// parse categories first
|
||||
dat, err := getFileContent("categories.yaml")
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to read categories.yaml"))
|
||||
}
|
||||
|
||||
h := md5.New()
|
||||
|
||||
cats := []WorkTemplateCategoryWithMD5{} // meow
|
||||
err = yaml.Unmarshal(dat, &cats)
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to unmarshal categories.yaml"))
|
||||
}
|
||||
|
||||
// validate categories
|
||||
categoryIds := map[string]struct{}{}
|
||||
for id := range cats {
|
||||
cat := cats[id]
|
||||
|
||||
if cat.ID == "" && cat.Name == "" {
|
||||
// skip empty array element
|
||||
continue
|
||||
}
|
||||
|
||||
if cat.ID == "" {
|
||||
log.Fatal(errors.New("category ID cannot be empty"))
|
||||
}
|
||||
if cat.Name == "" {
|
||||
log.Fatal(errors.New("category name cannot be empty"))
|
||||
}
|
||||
categoryIds[cat.ID] = struct{}{}
|
||||
|
||||
h.Write([]byte(cat.ID))
|
||||
cats[id].MD5 = fmt.Sprintf("%x", h.Sum(nil))
|
||||
h.Reset()
|
||||
}
|
||||
|
||||
dat, err = getFileContent("templates.yaml")
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to read templates.yaml"))
|
||||
}
|
||||
|
||||
dec := yaml.NewDecoder(bytes.NewReader(dat))
|
||||
ts := []WorkTemplateWithMD5{}
|
||||
for {
|
||||
t := worktemplates.WorkTemplate{}
|
||||
err = dec.Decode(&t)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
if t.ID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
h.Write([]byte(t.ID))
|
||||
err = t.Validate(categoryIds)
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to validate template"))
|
||||
}
|
||||
|
||||
ts = append(ts, WorkTemplateWithMD5{
|
||||
WorkTemplate: t,
|
||||
MD5: fmt.Sprintf("%x", h.Sum(nil)),
|
||||
})
|
||||
h.Reset()
|
||||
}
|
||||
|
||||
code := bytes.NewBuffer(nil)
|
||||
tmpl, err := template.New("worktemplates").Parse(tpl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tmpl.Execute(code, struct {
|
||||
Templates []WorkTemplateWithMD5
|
||||
Categories []WorkTemplateCategoryWithMD5
|
||||
}{
|
||||
Templates: ts,
|
||||
Categories: cats,
|
||||
})
|
||||
|
||||
formattedCode, err := imports.Process(path.Join("worktemplate_generated.go"), code.Bytes(), &imports.Options{Comments: true})
|
||||
if err != nil {
|
||||
log.Fatal(errors.Wrap(err, "failed to format code"))
|
||||
}
|
||||
|
||||
err = os.WriteFile(path.Join("worktemplate_generated.go"), formattedCode, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// print all translatable content
|
||||
fmt.Println("\nTranslation helpers:\n====================")
|
||||
for _, t := range ts {
|
||||
translationHelper(t.Description.Channel)
|
||||
translationHelper(t.Description.Board)
|
||||
translationHelper(t.Description.Playbook)
|
||||
translationHelper(t.Description.Integration)
|
||||
}
|
||||
}
|
||||
|
||||
var translationHelperTemplate = `{
|
||||
"id": %q,
|
||||
"translation": %q
|
||||
},`
|
||||
|
||||
func translationHelper(t *worktemplates.TranslatableString) {
|
||||
if t != nil && t.ID != "" && t.DefaultMessage != "" {
|
||||
fmt.Printf(translationHelperTemplate, t.ID, t.DefaultMessage)
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed worktemplate.tmpl
|
||||
var tpl string
|
||||
101
app/worktemplates/generator/worktemplate.tmpl
Обычный файл
101
app/worktemplates/generator/worktemplate.tmpl
Обычный файл
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make generate-worktemplates"
|
||||
// DO NOT EDIT
|
||||
|
||||
package worktemplates
|
||||
|
||||
func init() {
|
||||
{{- range .Categories}}
|
||||
registerWorkTemplateCategory("{{.ID}}", wtc{{.MD5}})
|
||||
{{- end -}}
|
||||
{{range .Templates}}
|
||||
registerWorkTemplate("{{.ID}}", wt{{.MD5}})
|
||||
{{- end}}
|
||||
|
||||
// Register categories strings
|
||||
{{range .Categories -}}
|
||||
_ = T("{{.Name}}")
|
||||
{{end}}
|
||||
|
||||
// Register translation strings
|
||||
{{range .Templates -}}
|
||||
{{if and (.Description.Channel) (ne .Description.Channel.ID "")}}_ = T("{{.Description.Channel.ID}}")
|
||||
{{end -}}
|
||||
{{if and (.Description.Board) (ne .Description.Board.ID "")}}_ = T("{{.Description.Board.ID}}")
|
||||
{{end -}}
|
||||
{{if and (.Description.Playbook) (ne .Description.Playbook.ID "")}}_ = T("{{.Description.Playbook.ID}}")
|
||||
{{end -}}
|
||||
{{if and (.Description.Integration) (ne .Description.Integration.ID "")}}_ = T("{{.Description.Integration.ID}}")
|
||||
{{end -}}
|
||||
{{end -}}
|
||||
}
|
||||
|
||||
{{range .Categories}}
|
||||
var wtc{{.MD5}} = &WorkTemplateCategory{
|
||||
ID: "{{.ID}}",
|
||||
Name: "{{.Name}}",
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{range .Templates}}
|
||||
var wt{{.MD5}} = &WorkTemplate{
|
||||
ID: "{{.ID}}",
|
||||
Category: "{{.Category}}",
|
||||
UseCase: "{{.UseCase}}",
|
||||
Illustration: "{{.Illustration}}",
|
||||
Visibility: "{{.Visibility}}",
|
||||
{{if .FeatureFlag}}FeatureFlag: &FeatureFlag{
|
||||
Name: "{{.FeatureFlag.Name}}",
|
||||
Value: "{{.FeatureFlag.Value}}",
|
||||
},{{end}}
|
||||
Description: Description{
|
||||
{{if .Description.Channel}}Channel: &TranslatableString{
|
||||
ID: "{{.Description.Channel.ID}}",
|
||||
DefaultMessage: "{{.Description.Channel.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Channel.Illustration}}",
|
||||
},{{end}}
|
||||
{{if .Description.Board}}Board: &TranslatableString{
|
||||
ID: "{{.Description.Board.ID}}",
|
||||
DefaultMessage: "{{.Description.Board.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Board.Illustration}}",
|
||||
},{{end}}
|
||||
{{if .Description.Playbook}}Playbook: &TranslatableString{
|
||||
ID: "{{.Description.Playbook.ID}}",
|
||||
DefaultMessage: "{{.Description.Playbook.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Playbook.Illustration}}",
|
||||
},{{end}}
|
||||
{{if .Description.Integration}}Integration: &TranslatableString{
|
||||
ID: "{{.Description.Integration.ID}}",
|
||||
DefaultMessage: "{{.Description.Integration.DefaultMessage}}",
|
||||
Illustration: "{{.Description.Integration.Illustration}}",
|
||||
},{{end}}
|
||||
},
|
||||
Content: []Content{
|
||||
{{range .Content}}{
|
||||
{{if .Channel}}Channel: &Channel{
|
||||
ID: "{{.Channel.ID}}",
|
||||
Name: "{{.Channel.Name}}",
|
||||
Purpose: "{{.Channel.Purpose}}",
|
||||
Playbook: "{{.Channel.Playbook}}",
|
||||
Illustration: "{{.Channel.Illustration}}",
|
||||
},{{end}}{{if .Board}}Board: &Board{
|
||||
ID: "{{.Board.ID}}",
|
||||
Template: "{{.Board.Template}}",
|
||||
Name: "{{.Board.Name}}",
|
||||
Channel: "{{.Board.Channel}}",
|
||||
Illustration: "{{.Board.Illustration}}",
|
||||
},{{end}}{{if .Playbook}}Playbook: &Playbook{
|
||||
Template: "{{.Playbook.Template}}",
|
||||
Name: "{{.Playbook.Name}}",
|
||||
ID: "{{.Playbook.ID}}",
|
||||
Illustration: "{{.Playbook.Illustration}}",
|
||||
},{{end}}{{if .Integration}}Integration: &Integration{
|
||||
ID: "{{.Integration.ID}}",
|
||||
},{{end}}
|
||||
},
|
||||
{{end}}
|
||||
},
|
||||
}
|
||||
{{end}}
|
||||
46
app/worktemplates/templates.yaml
Обычный файл
46
app/worktemplates/templates.yaml
Обычный файл
@@ -0,0 +1,46 @@
|
||||
id: "product_teams/feature_release:v1"
|
||||
category: product_teams
|
||||
useCase: Feature Release
|
||||
illustration: https://via.placeholder.com/204x123.png
|
||||
visibility: public
|
||||
description:
|
||||
channel:
|
||||
id: "worktemplate.product_teams.feature_release.description.channel"
|
||||
defaultMessage: "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots."
|
||||
board:
|
||||
id: "worktemplate.product_teams.feature_release.description.board"
|
||||
defaultMessage: "Use our Meeting Agenda board template for recurring meetings like standup and our Project Tasks board to manage the progress of tasks along the way."
|
||||
playbook:
|
||||
id: "worktemplate.product_teams.feature_release.description.playbook"
|
||||
defaultMessage: "Create transparent workflows across development teams to ensure your feature development process is seamless."
|
||||
integration:
|
||||
id: "worktemplate.product_teams.feature_release.description.integration"
|
||||
defaultMessage: "Increase productivity in your channel by integrating a Jira bot and Github bot. These will be downloaded for you."
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Integrations"
|
||||
content:
|
||||
- channel:
|
||||
id: feature-release
|
||||
name: Feature Release
|
||||
playbook: product-release-playbook # playbook id. if set the channel will be created by the playbook run.
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Channel+feature+release"
|
||||
- board:
|
||||
id: "board-meeting-agenda"
|
||||
template: "meeting agenda|bwps66irhr7b9dxgayf9kz33g5o" # <-- have to find a way to target the board template... could hardcode the ids but need to verify that they don't change?
|
||||
name: Meeting Agenda
|
||||
channel: feature-release # <-- optional. we use the channel "id" from above
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Board+meeting+agenda"
|
||||
- board:
|
||||
id: "board-project-task"
|
||||
template: "project task|bmttiziw35irgtmztewd9upyqdy"
|
||||
name: project task board
|
||||
channel: feature-release
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Board+project+task"
|
||||
- playbook:
|
||||
template: "product release" # <-- playbooks templates don't have ids, have to rely on name
|
||||
name: "Feature release"
|
||||
id: product-release-playbook
|
||||
illustration: "https://via.placeholder.com/509x352.png?text=Playbook+feature+release"
|
||||
- integration:
|
||||
id: jira
|
||||
- integration:
|
||||
id: github
|
||||
342
app/worktemplates/types.go
Обычный файл
342
app/worktemplates/types.go
Обычный файл
@@ -0,0 +1,342 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package worktemplates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type WorkTemplateCategory struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type WorkTemplate struct {
|
||||
ID string `yaml:"id"`
|
||||
Category string `yaml:"category"`
|
||||
UseCase string `yaml:"useCase"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
Visibility string `yaml:"visibility"`
|
||||
FeatureFlag *FeatureFlag `yaml:"featureFlag,omitempty"`
|
||||
Description Description `yaml:"description"`
|
||||
Content []Content `yaml:"content"`
|
||||
}
|
||||
|
||||
func (wt WorkTemplate) ToModelWorkTemplate(t i18n.TranslateFunc) *model.WorkTemplate {
|
||||
mwt := &model.WorkTemplate{
|
||||
ID: wt.ID,
|
||||
Category: wt.Category,
|
||||
UseCase: wt.UseCase,
|
||||
Illustration: wt.Illustration,
|
||||
Visibility: wt.Visibility,
|
||||
}
|
||||
|
||||
if wt.FeatureFlag != nil {
|
||||
mwt.FeatureFlag = &model.WorkTemplateFeatureFlag{
|
||||
Name: wt.FeatureFlag.Name,
|
||||
Value: wt.FeatureFlag.Value,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Channel != nil {
|
||||
mwt.Description.Channel = &model.DescriptionContent{
|
||||
Message: wt.Description.Channel.Translate(t),
|
||||
Illustration: wt.Description.Channel.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Board != nil {
|
||||
mwt.Description.Board = &model.DescriptionContent{
|
||||
Message: wt.Description.Board.Translate(t),
|
||||
Illustration: wt.Description.Board.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Playbook != nil {
|
||||
mwt.Description.Playbook = &model.DescriptionContent{
|
||||
Message: wt.Description.Playbook.Translate(t),
|
||||
Illustration: wt.Description.Playbook.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
if wt.Description.Integration != nil {
|
||||
mwt.Description.Integration = &model.DescriptionContent{
|
||||
Message: wt.Description.Integration.Translate(t),
|
||||
Illustration: wt.Description.Integration.Illustration,
|
||||
}
|
||||
}
|
||||
|
||||
for _, content := range wt.Content {
|
||||
if content.Channel != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Channel: &model.WorkTemplateChannel{
|
||||
ID: content.Channel.ID,
|
||||
Name: content.Channel.Name,
|
||||
Purpose: content.Channel.Purpose,
|
||||
Playbook: content.Channel.Playbook,
|
||||
Illustration: content.Channel.Illustration,
|
||||
},
|
||||
})
|
||||
}
|
||||
if content.Board != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Board: &model.WorkTemplateBoard{
|
||||
ID: content.Board.ID,
|
||||
Name: content.Board.Name,
|
||||
Template: content.Board.Template,
|
||||
Channel: content.Board.Channel,
|
||||
Illustration: content.Board.Illustration,
|
||||
},
|
||||
})
|
||||
}
|
||||
if content.Playbook != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Playbook: &model.WorkTemplatePlaybook{
|
||||
ID: content.Playbook.ID,
|
||||
Name: content.Playbook.Name,
|
||||
Template: content.Playbook.Template,
|
||||
Illustration: content.Playbook.Illustration,
|
||||
},
|
||||
})
|
||||
}
|
||||
if content.Integration != nil {
|
||||
mwt.Content = append(mwt.Content, model.WorkTemplateContent{
|
||||
Integration: &model.WorkTemplateIntegration{
|
||||
ID: content.Integration.ID,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return mwt
|
||||
}
|
||||
|
||||
func (wt WorkTemplate) Validate(categoryIds map[string]struct{}) error {
|
||||
if wt.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if wt.Category == "" {
|
||||
return errors.New("category is required")
|
||||
}
|
||||
if _, ok := categoryIds[wt.Category]; !ok {
|
||||
return fmt.Errorf("category %s does not exist", wt.Category)
|
||||
}
|
||||
if wt.UseCase == "" {
|
||||
return errors.New("useCase is required")
|
||||
}
|
||||
if wt.Illustration == "" {
|
||||
return errors.New("illustration is required")
|
||||
}
|
||||
if wt.Visibility == "" {
|
||||
return errors.New("visibility is required")
|
||||
}
|
||||
hasChannel := false
|
||||
hasBoard := false
|
||||
hasPlaybook := false
|
||||
hasIntegration := false
|
||||
foundChannels := map[string]struct{}{}
|
||||
foundPlaybooks := map[string]struct{}{}
|
||||
foundBoards := map[string]struct{}{}
|
||||
foundIntegrations := map[string]struct{}{}
|
||||
mustHaveChannels := []string{}
|
||||
mustHavePlaybooks := []string{}
|
||||
|
||||
currentIdx := 0
|
||||
for _, content := range wt.Content {
|
||||
if content.Channel != nil {
|
||||
hasChannel = true
|
||||
if cErr := content.Channel.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundChannels[content.Channel.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate channel %s found", content.Channel.ID), currentIdx)
|
||||
}
|
||||
foundChannels[content.Channel.ID] = struct{}{}
|
||||
|
||||
if content.Channel.Playbook != "" {
|
||||
mustHavePlaybooks = append(mustHavePlaybooks, content.Channel.Playbook)
|
||||
}
|
||||
}
|
||||
|
||||
if content.Board != nil {
|
||||
hasBoard = true
|
||||
if cErr := content.Board.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundBoards[content.Board.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate board %s found", content.Board.ID), currentIdx)
|
||||
}
|
||||
foundBoards[content.Board.ID] = struct{}{}
|
||||
|
||||
if content.Board.Channel != "" {
|
||||
mustHaveChannels = append(mustHaveChannels, content.Board.Channel)
|
||||
}
|
||||
}
|
||||
if content.Playbook != nil {
|
||||
hasPlaybook = true
|
||||
if cErr := content.Playbook.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundPlaybooks[content.Playbook.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate playbook %s found", content.Playbook.ID), currentIdx)
|
||||
}
|
||||
foundPlaybooks[content.Playbook.ID] = struct{}{}
|
||||
}
|
||||
if content.Integration != nil {
|
||||
hasIntegration = true
|
||||
if cErr := content.Integration.Validate(); cErr != nil {
|
||||
return wrapContentError(cErr, currentIdx)
|
||||
}
|
||||
if _, ok := foundIntegrations[content.Integration.ID]; ok {
|
||||
return wrapContentError(fmt.Errorf("duplicate integration %s found", content.Integration.ID), currentIdx)
|
||||
}
|
||||
foundIntegrations[content.Integration.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if hasChannel && wt.Description.Channel == nil {
|
||||
return errors.New("description.channel is required")
|
||||
}
|
||||
if hasBoard && wt.Description.Board == nil {
|
||||
return errors.New("description.board is required")
|
||||
}
|
||||
if hasPlaybook && wt.Description.Playbook == nil {
|
||||
return errors.New("description.playbook is required")
|
||||
}
|
||||
if hasIntegration && wt.Description.Integration == nil {
|
||||
return errors.New("description.integration is required")
|
||||
}
|
||||
|
||||
for _, channel := range mustHaveChannels {
|
||||
if _, ok := foundChannels[channel]; !ok {
|
||||
return fmt.Errorf("channel %s is required", channel)
|
||||
}
|
||||
}
|
||||
|
||||
for _, playbook := range mustHavePlaybooks {
|
||||
if _, ok := foundPlaybooks[playbook]; !ok {
|
||||
return fmt.Errorf("playbook %s is required", playbook)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type FeatureFlag struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type TranslatableString struct {
|
||||
ID string `yaml:"id"`
|
||||
DefaultMessage string `yaml:"defaultMessage"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (ts TranslatableString) Translate(t i18n.TranslateFunc) string {
|
||||
if ts.ID != "" {
|
||||
msg := t(ts.ID)
|
||||
if msg != ts.ID && msg != "" {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
return ts.DefaultMessage
|
||||
}
|
||||
|
||||
type Description struct {
|
||||
Channel *TranslatableString `yaml:"channel"`
|
||||
Board *TranslatableString `yaml:"board"`
|
||||
Playbook *TranslatableString `yaml:"playbook"`
|
||||
Integration *TranslatableString `yaml:"integration"`
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Purpose string `yaml:"purpose"`
|
||||
Playbook string `yaml:"playbook"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (c *Channel) Validate() error {
|
||||
if c.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if c.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Board struct {
|
||||
ID string `yaml:"id"`
|
||||
Template string `yaml:"template"`
|
||||
Name string `yaml:"name"`
|
||||
Channel string `yaml:"channel"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (b Board) Validate() error {
|
||||
if b.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if b.Template == "" {
|
||||
return errors.New("template is required")
|
||||
}
|
||||
if b.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Playbook struct {
|
||||
Template string `yaml:"template"`
|
||||
Name string `yaml:"name"`
|
||||
ID string `yaml:"id"`
|
||||
Illustration string `yaml:"illustration"`
|
||||
}
|
||||
|
||||
func (p *Playbook) Validate() error {
|
||||
if p.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if p.Template == "" {
|
||||
return errors.New("template is required")
|
||||
}
|
||||
if p.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Integration struct {
|
||||
ID string `yaml:"id"`
|
||||
}
|
||||
|
||||
func (i *Integration) Validate() error {
|
||||
if i.ID == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Channel *Channel `yaml:"channel,omitempty"`
|
||||
Board *Board `yaml:"board,omitempty"`
|
||||
Playbook *Playbook `yaml:"playbook,omitempty"`
|
||||
Integration *Integration `yaml:"integration,omitempty"`
|
||||
}
|
||||
|
||||
func wrapContentError(err error, index int) error {
|
||||
return errors.Wrapf(err, "content #%d validation failed", index)
|
||||
}
|
||||
104
app/worktemplates/worktemplate_generated.go
Обычный файл
104
app/worktemplates/worktemplate_generated.go
Обычный файл
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make generate-worktemplates"
|
||||
// DO NOT EDIT
|
||||
|
||||
package worktemplates
|
||||
|
||||
func init() {
|
||||
registerWorkTemplateCategory("product_teams", wtc846b565cd80043537945134a54812e07)
|
||||
registerWorkTemplate("product_teams/feature_release:v1", wt00a1b44a5831c0a3acb14787b3fdd352)
|
||||
|
||||
// Register categories strings
|
||||
_ = T("worktemplate.category.product_teams")
|
||||
|
||||
// Register translation strings
|
||||
_ = T("worktemplate.product_teams.feature_release.description.channel")
|
||||
_ = T("worktemplate.product_teams.feature_release.description.board")
|
||||
_ = T("worktemplate.product_teams.feature_release.description.playbook")
|
||||
_ = T("worktemplate.product_teams.feature_release.description.integration")
|
||||
}
|
||||
|
||||
var wtc846b565cd80043537945134a54812e07 = &WorkTemplateCategory{
|
||||
ID: "product_teams",
|
||||
Name: "worktemplate.category.product_teams",
|
||||
}
|
||||
|
||||
var wt00a1b44a5831c0a3acb14787b3fdd352 = &WorkTemplate{
|
||||
ID: "product_teams/feature_release:v1",
|
||||
Category: "product_teams",
|
||||
UseCase: "Feature Release",
|
||||
Illustration: "https://via.placeholder.com/204x123.png",
|
||||
Visibility: "public",
|
||||
|
||||
Description: Description{
|
||||
Channel: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.channel",
|
||||
DefaultMessage: "Chat with your team in a Feature Release channel that connects easily with your boards, playbooks and app bots.",
|
||||
Illustration: "",
|
||||
},
|
||||
Board: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.board",
|
||||
DefaultMessage: "Use our Meeting Agenda board template for recurring meetings like standup and our Project Tasks board to manage the progress of tasks along the way.",
|
||||
Illustration: "",
|
||||
},
|
||||
Playbook: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.playbook",
|
||||
DefaultMessage: "Create transparent workflows across development teams to ensure your feature development process is seamless.",
|
||||
Illustration: "",
|
||||
},
|
||||
Integration: &TranslatableString{
|
||||
ID: "worktemplate.product_teams.feature_release.description.integration",
|
||||
DefaultMessage: "Increase productivity in your channel by integrating a Jira bot and Github bot. These will be downloaded for you.",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Integrations",
|
||||
},
|
||||
},
|
||||
Content: []Content{
|
||||
{
|
||||
Channel: &Channel{
|
||||
ID: "feature-release",
|
||||
Name: "Feature Release",
|
||||
Purpose: "",
|
||||
Playbook: "product-release-playbook",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Channel+feature+release",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &Board{
|
||||
ID: "board-meeting-agenda",
|
||||
Template: "meeting agenda|bwps66irhr7b9dxgayf9kz33g5o",
|
||||
Name: "Meeting Agenda",
|
||||
Channel: "feature-release",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Board+meeting+agenda",
|
||||
},
|
||||
},
|
||||
{
|
||||
Board: &Board{
|
||||
ID: "board-project-task",
|
||||
Template: "project task|bmttiziw35irgtmztewd9upyqdy",
|
||||
Name: "project task board",
|
||||
Channel: "feature-release",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Board+project+task",
|
||||
},
|
||||
},
|
||||
{
|
||||
Playbook: &Playbook{
|
||||
Template: "product release",
|
||||
Name: "Feature release",
|
||||
ID: "product-release-playbook",
|
||||
Illustration: "https://via.placeholder.com/509x352.png?text=Playbook+feature+release",
|
||||
},
|
||||
},
|
||||
{
|
||||
Integration: &Integration{
|
||||
ID: "jira",
|
||||
},
|
||||
},
|
||||
{
|
||||
Integration: &Integration{
|
||||
ID: "github",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
37
app/worktemplates/worktemplates.go
Обычный файл
37
app/worktemplates/worktemplates.go
Обычный файл
@@ -0,0 +1,37 @@
|
||||
//go:generate go run generator/main.go
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package worktemplates
|
||||
|
||||
var OrderedWorkTemplates = []*WorkTemplate{}
|
||||
var OrderedWorkTemplateCategories = []*WorkTemplateCategory{}
|
||||
|
||||
// T is a placeholder to allow the translation tool to register the strings
|
||||
func T(id string) string {
|
||||
return id
|
||||
}
|
||||
|
||||
func registerWorkTemplate(id string, wt *WorkTemplate) {
|
||||
OrderedWorkTemplates = append(OrderedWorkTemplates, wt)
|
||||
}
|
||||
|
||||
func registerWorkTemplateCategory(id string, wtc *WorkTemplateCategory) {
|
||||
OrderedWorkTemplateCategories = append(OrderedWorkTemplateCategories, wtc)
|
||||
}
|
||||
|
||||
func ListCategories() ([]*WorkTemplateCategory, error) {
|
||||
return OrderedWorkTemplateCategories, nil
|
||||
}
|
||||
|
||||
func ListByCategory(category string) ([]*WorkTemplate, error) {
|
||||
wts := []*WorkTemplate{}
|
||||
for i := range OrderedWorkTemplates {
|
||||
if OrderedWorkTemplates[i].Category == category {
|
||||
wts = append(wts, OrderedWorkTemplates[i])
|
||||
}
|
||||
}
|
||||
|
||||
return wts, nil
|
||||
}
|
||||
125
app/worktemplates_test.go
Обычный файл
125
app/worktemplates_test.go
Обычный файл
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
|
||||
)
|
||||
|
||||
func TestGetWorkTemplateCategories(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
assert := require.New(t)
|
||||
|
||||
worktemplates.OrderedWorkTemplateCategories = wtGetCategories()
|
||||
|
||||
categories, appErr := th.App.GetWorkTemplateCategories(wtTranslationFunc)
|
||||
assert.Nil(appErr)
|
||||
assert.Len(categories, 2)
|
||||
assert.Equal("Translated test.1", categories[0].Name)
|
||||
assert.Equal("Translated test.2", categories[1].Name)
|
||||
}
|
||||
|
||||
func TestGetWorkTemplatesByCategory(t *testing.T) {
|
||||
// Setup
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
assert := require.New(t)
|
||||
|
||||
existingFFkey := "test-feature-flag"
|
||||
existingFFvalue := "true"
|
||||
ff := map[string]string{
|
||||
existingFFkey: existingFFvalue,
|
||||
}
|
||||
|
||||
worktemplates.OrderedWorkTemplateCategories = wtGetCategories()
|
||||
firstCat := worktemplates.OrderedWorkTemplateCategories[0]
|
||||
worktemplates.OrderedWorkTemplates = []*worktemplates.WorkTemplate{
|
||||
{
|
||||
ID: "test-template",
|
||||
Category: firstCat.ID,
|
||||
UseCase: "test use case",
|
||||
Description: worktemplates.Description{
|
||||
Channel: &worktemplates.TranslatableString{
|
||||
ID: "test-template-channel-description",
|
||||
DefaultMessage: "test template channel description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // this one should not be returned because of the FF
|
||||
ID: "test-template-2",
|
||||
Category: firstCat.ID,
|
||||
UseCase: "test use case 2",
|
||||
FeatureFlag: &worktemplates.FeatureFlag{
|
||||
Name: "nonexistant-random-test-feature-flag",
|
||||
Value: "hi",
|
||||
},
|
||||
Description: worktemplates.Description{
|
||||
Channel: &worktemplates.TranslatableString{
|
||||
ID: "test-template-2-channel-description",
|
||||
DefaultMessage: "test template 2 channel description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // this one should be present and match the FF
|
||||
ID: "test-template-3",
|
||||
Category: firstCat.ID,
|
||||
UseCase: "test use case 3",
|
||||
FeatureFlag: &worktemplates.FeatureFlag{
|
||||
Name: existingFFkey,
|
||||
Value: existingFFvalue,
|
||||
},
|
||||
Description: worktemplates.Description{
|
||||
Channel: &worktemplates.TranslatableString{
|
||||
ID: "unknown", // simulating an unknown translation, we return the default message in this case
|
||||
DefaultMessage: "default message picked for unknown",
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // this one should not be returned because of the category
|
||||
ID: "test-template-4",
|
||||
Category: "cat-test2",
|
||||
UseCase: "test use case 4",
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
worktemplates, appErr := th.App.GetWorkTemplates(firstCat.ID, ff, wtTranslationFunc)
|
||||
|
||||
// Assert
|
||||
assert.Nil(appErr)
|
||||
assert.Len(worktemplates, 2)
|
||||
// assert the correct work templates have been returned
|
||||
assert.Equal("test-template", worktemplates[0].ID)
|
||||
assert.Equal("test-template-3", worktemplates[1].ID)
|
||||
// assert the descriptions have been translated
|
||||
assert.Equal("Translated test-template-channel-description", worktemplates[0].Description.Channel.Message)
|
||||
assert.Equal("default message picked for unknown", worktemplates[1].Description.Channel.Message)
|
||||
}
|
||||
|
||||
// helpers
|
||||
func wtTranslationFunc(id string, args ...interface{}) string {
|
||||
if id == "unknown" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "Translated " + id
|
||||
}
|
||||
|
||||
func wtGetCategories() []*worktemplates.WorkTemplateCategory {
|
||||
return []*worktemplates.WorkTemplateCategory{
|
||||
{
|
||||
ID: "cat-test1",
|
||||
Name: "test.1",
|
||||
},
|
||||
{
|
||||
ID: "cat-test2",
|
||||
Name: "test.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user