Merge branch 'master' into mpa-playbooks

Этот коммит содержится в:
Giorgi Bochorishvili
2022-12-10 19:21:08 +04:00
родитель 1a2c08f1c6 3b043c1f12
Коммит 496fe73f10
243 изменённых файлов: 12751 добавлений и 4439 удалений

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

@@ -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)

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

@@ -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)
}

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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, &notFoundErr) {
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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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)

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

@@ -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)
}

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

@@ -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

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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.

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

@@ -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() {

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

@@ -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)

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

@@ -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) {

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -0,0 +1,2 @@
- id: product_teams
name: worktemplate.category.product_teams

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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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&#43;feature&#43;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&#43;meeting&#43;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&#43;project&#43;task",
},
},
{
Playbook: &Playbook{
Template: "product release",
Name: "Feature release",
ID: "product-release-playbook",
Illustration: "https://via.placeholder.com/509x352.png?text=Playbook&#43;feature&#43;release",
},
},
{
Integration: &Integration{
ID: "jira",
},
},
{
Integration: &Integration{
ID: "github",
},
},
},
}

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 Обычный файл
Просмотреть файл

@@ -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",
},
}
}