[MM-48029] notify admin for plugin install from work templates (#22007)

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Julien Tant <julien@craftyx.fr>
Этот коммит содержится в:
Muhammad S
2023-02-09 02:08:33 +05:00
коммит произвёл GitHub
родитель a72dd120b7
Коммит 3ae70591d0
26 изменённых файлов: 639 добавлений и 100 удалений

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

@@ -7,10 +7,13 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
) )
func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) { func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) {
mlog.Info("enter handleNotifyAdmin")
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
err := json.NewDecoder(r.Body).Decode(&notifyAdminRequest) err := json.NewDecoder(r.Body).Decode(&notifyAdminRequest)
if err != nil { if err != nil {
@@ -24,11 +27,14 @@ func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err = appErr c.Err = appErr
return return
} }
mlog.Info("exit handleNotifyAdmin")
ReturnStatusOK(w) ReturnStatusOK(w)
} }
func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Request) { func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Request) {
mlog.Info("enter handleTriggerNotifyAdminPosts")
if !*c.App.Config().ServiceSettings.EnableAPITriggerAdminNotifications { if !*c.App.Config().ServiceSettings.EnableAPITriggerAdminNotifications {
c.Err = model.NewAppError("Api4.handleTriggerNotifyAdminPosts", "api.cloud.app_error", nil, "Manual triggering of notifications not allowed", http.StatusForbidden) c.Err = model.NewAppError("Api4.handleTriggerNotifyAdminPosts", "api.cloud.app_error", nil, "Manual triggering of notifications not allowed", http.StatusForbidden)
return return
@@ -53,5 +59,7 @@ func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Re
return return
} }
mlog.Info("exit handleTriggerNotifyAdminPosts")
ReturnStatusOK(w) ReturnStatusOK(w)
} }

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

@@ -573,7 +573,7 @@ type AppIface interface {
FillInChannelsProps(c request.CTX, channelList model.ChannelList) *model.AppError FillInChannelsProps(c request.CTX, channelList model.ChannelList) *model.AppError
FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError) FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError)
FindTeamByName(name string) bool FindTeamByName(name string) bool
FinishSendAdminNotifyPost(trial bool, now int64) FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData)
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
GeneratePublicLink(siteURL string, info *model.FileInfo) string GeneratePublicLink(siteURL string, info *model.FileInfo) string
GenerateSupportPacket() []model.FileData GenerateSupportPacket() []model.FileData
@@ -1167,7 +1167,7 @@ type AppIface interface {
UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError)
UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError)
UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError)
UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostPaidFeature) bool UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
UserIsFirstAdmin(user *model.User) bool UserIsFirstAdmin(user *model.User) bool
VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError

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

@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/mattermost/mattermost-server/v6/app/request" "github.com/mattermost/mattermost-server/v6/app/request"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
@@ -19,14 +20,16 @@ import (
const lastTrialNotificationTimeStamp = "LAST_TRIAL_NOTIFICATION_TIMESTAMP" const lastTrialNotificationTimeStamp = "LAST_TRIAL_NOTIFICATION_TIMESTAMP"
const lastUpgradeNotificationTimeStamp = "LAST_UPGRADE_NOTIFICATION_TIMESTAMP" const lastUpgradeNotificationTimeStamp = "LAST_UPGRADE_NOTIFICATION_TIMESTAMP"
const defaultNotifyAdminCoolOffDays = 14 const defaultNotifyAdminCoolOffDays = 0.0104166667 // this is a temp change
func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError { func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
requiredFeature := notifyData.RequiredFeature requiredFeature := notifyData.RequiredFeature
requiredPlan := notifyData.RequiredPlan requiredPlan := notifyData.RequiredPlan
trial := notifyData.TrialNotification trial := notifyData.TrialNotification
if a.UserAlreadyNotifiedOnRequiredFeature(userId, requiredFeature) { isUserAlreadyNotified := a.UserAlreadyNotifiedOnRequiredFeature(userId, requiredFeature)
mlog.Info("SaveAdminNotification")
if isUserAlreadyNotified {
return model.NewAppError("app.SaveAdminNotification", "api.cloud.notify_admin_to_upgrade_error.already_notified", nil, "", http.StatusForbidden) return model.NewAppError("app.SaveAdminNotification", "api.cloud.notify_admin_to_upgrade_error.already_notified", nil, "", http.StatusForbidden)
} }
@@ -36,6 +39,7 @@ func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdmin
RequiredFeature: requiredFeature, RequiredFeature: requiredFeature,
Trial: trial, Trial: trial,
}) })
if appErr != nil { if appErr != nil {
return appErr return appErr
} }
@@ -57,7 +61,9 @@ func (a *App) DoCheckForAdminNotifications(trial bool) *model.AppError {
} }
func (a *App) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) { func (a *App) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) {
mlog.Info("Trying to save NotifyAdmin data")
d, err := a.Srv().Store().NotifyAdmin().Save(data) d, err := a.Srv().Store().NotifyAdmin().Save(data)
mlog.Info("NotifyAdmin data saved")
if err != nil { if err != nil {
var nfErr *store.ErrNotFound var nfErr *store.ErrNotFound
switch { switch {
@@ -67,6 +73,7 @@ func (a *App) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdm
return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, err.Error(), http.StatusInternalServerError) return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, err.Error(), http.StatusInternalServerError)
} }
} }
mlog.Info("return from SaveAdminNotifyData")
return d, nil return d, nil
} }
@@ -81,6 +88,8 @@ func filterNotificationData(data []*model.NotifyAdminData, test func(*model.Noti
} }
func (a *App) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError { func (a *App) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError {
mlog.Info("enter SendNotifyAdminPosts")
if !a.CanNotifyAdmin(trial) { if !a.CanNotifyAdmin(trial) {
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, "Cannot notify yet", http.StatusForbidden) return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, "Cannot notify yet", http.StatusForbidden)
} }
@@ -103,6 +112,8 @@ func (a *App) SendNotifyAdminPosts(c *request.Context, workspaceName string, cur
now := model.GetMillis() now := model.GetMillis()
data, err := a.Srv().Store().NotifyAdmin().Get(trial) data, err := a.Srv().Store().NotifyAdmin().Get(trial)
mlog.Info("SendNotifyAdminPosts")
if err != nil { if err != nil {
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, err.Error(), http.StatusInternalServerError) return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, err.Error(), http.StatusInternalServerError)
} }
@@ -110,58 +121,99 @@ func (a *App) SendNotifyAdminPosts(c *request.Context, workspaceName string, cur
data = filterNotificationData(data, func(nad *model.NotifyAdminData) bool { return nad.RequiredPlan != currentSKU }) data = filterNotificationData(data, func(nad *model.NotifyAdminData) bool { return nad.RequiredPlan != currentSKU })
if len(data) == 0 { if len(data) == 0 {
mlog.Warn("No notification data available") a.Log().Warn("No notification data available")
return nil return nil
} }
userBasedData := a.groupNotifyAdminByUser(data) userBasedPaidFeatureData, userBasedPluginData := a.groupNotifyAdminByUser(data)
featureBasedData := a.groupNotifyAdminByFeature(data) featureBasedData := a.groupNotifyAdminByPaidFeature(data)
props := make(model.StringInterface) pluginBasedData := a.groupNotifyAdminByPlugin(data)
for _, admin := range sysadmins { for _, admin := range sysadmins {
T := i18n.GetUserTranslations(admin.Locale) if len(userBasedPaidFeatureData) > 0 && len(featureBasedData) > 0 {
message := T("app.cloud.upgrade_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName}) a.upgradePlanAdminNotifyPost(c, workspaceName, userBasedPaidFeatureData, featureBasedData, systemBot, admin, trial)
if len(userBasedData) == 1 {
message = T("app.cloud.upgrade_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName}) // todo (allan): investigate if translations library can do this
}
if trial {
message = T("app.cloud.trial_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
if len(userBasedData) == 1 {
message = T("app.cloud.trial_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
}
} }
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id) if len(userBasedPluginData) > 0 {
if appErr != nil { mlog.Info("SendNotifyAdminPosts", mlog.String("length of user based plugin data", fmt.Sprint(len(userBasedPluginData))))
mlog.Warn("Error getting direct channel", mlog.Err(appErr))
continue
}
post := &model.Post{ a.pluginInstallAdminNotifyPost(c, userBasedPluginData, pluginBasedData, systemBot, admin)
Message: message,
UserId: systemBot.UserId,
ChannelId: channel.Id,
Type: fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
}
props["requested_features"] = featureBasedData
props["trial"] = trial
post.SetProps(props)
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
mlog.Warn("Error creating post", mlog.Err(appErr))
continue
} }
} }
a.FinishSendAdminNotifyPost(trial, now) a.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
return nil return nil
} }
func (a *App) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostPaidFeature) bool { func (a *App) pluginInstallAdminNotifyPost(c *request.Context, userBasedData map[string][]*model.NotifyAdminData, pluginBasedPluginData map[string][]*model.NotifyAdminData, systemBot *model.Bot, admin *model.User) {
props := make(model.StringInterface)
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id)
if appErr != nil {
a.Log().Warn("Error getting direct channel", mlog.Err(appErr))
return
}
post := &model.Post{
UserId: systemBot.UserId,
ChannelId: channel.Id,
Type: fmt.Sprintf("%spl_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
}
props["requested_plugins_by_plugin_ids"] = pluginBasedPluginData
props["requested_plugins_by_user_ids"] = userBasedData
post.SetProps(props)
mlog.Info("pluginInstallAdminNotifyPost: send props")
_, appErr = a.CreatePost(c, post, channel, false, true)
mlog.Info("pluginInstallAdminNotifyPost: post created")
if appErr != nil {
a.Log().Warn("Error creating post", mlog.Err(appErr))
}
}
func (a *App) upgradePlanAdminNotifyPost(c *request.Context, workspaceName string, userBasedData map[string][]*model.NotifyAdminData, featureBasedData map[model.MattermostFeature][]*model.NotifyAdminData, systemBot *model.Bot, admin *model.User, trial bool) {
props := make(model.StringInterface)
T := i18n.GetUserTranslations(admin.Locale)
message := T("app.cloud.upgrade_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
if len(userBasedData) == 1 {
message = T("app.cloud.upgrade_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName}) // todo (allan): investigate if translations library can do this
}
if trial {
message = T("app.cloud.trial_plan_bot_message", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
if len(userBasedData) == 1 {
message = T("app.cloud.trial_plan_bot_message_single", map[string]interface{}{"UsersNum": len(userBasedData), "WorkspaceName": workspaceName})
}
}
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id)
if appErr != nil {
a.Log().Warn("Error getting direct channel", mlog.Err(appErr))
return
}
post := &model.Post{
Message: message,
UserId: systemBot.UserId,
ChannelId: channel.Id,
Type: fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
}
props["requested_features"] = featureBasedData
props["trial"] = trial
post.SetProps(props)
_, appErr = a.CreatePost(c, post, channel, false, true)
if appErr != nil {
a.Log().Warn("Error creating post", mlog.Err(appErr))
}
}
func (a *App) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool {
data, err := a.Srv().Store().NotifyAdmin().GetDataByUserIdAndFeature(user, feature) data, err := a.Srv().Store().NotifyAdmin().GetDataByUserIdAndFeature(user, feature)
if err != nil { if err != nil {
return false return false
@@ -185,13 +237,13 @@ func (a *App) CanNotifyAdmin(trial bool) bool {
if errors.As(sysValErr, &nfErr) { // if no timestamps have been recorded before, system is free to notify if errors.As(sysValErr, &nfErr) { // if no timestamps have been recorded before, system is free to notify
return true return true
} }
mlog.Error("Cannot notify", mlog.Err(sysValErr)) a.Log().Error("Cannot notify", mlog.Err(sysValErr))
return false return false
} }
lastNotificationTimestamp, err := strconv.ParseFloat(sysVal.Value, 64) lastNotificationTimestamp, err := strconv.ParseFloat(sysVal.Value, 64)
if err != nil { if err != nil {
mlog.Error("Cannot notify", mlog.Err(err)) a.Log().Error("Cannot notify", mlog.Err(err))
return false return false
} }
@@ -205,7 +257,9 @@ func (a *App) CanNotifyAdmin(trial bool) bool {
return timeDiff >= int64(daysToMillis) return timeDiff >= int64(daysToMillis)
} }
func (a *App) FinishSendAdminNotifyPost(trial bool, now int64) { func (a *App) FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData) {
mlog.Info("FinishSendAdminNotifyPost")
systemVarName := lastUpgradeNotificationTimeStamp systemVarName := lastUpgradeNotificationTimeStamp
if trial { if trial {
systemVarName = lastTrialNotificationTimeStamp systemVarName = lastTrialNotificationTimeStamp
@@ -214,30 +268,64 @@ func (a *App) FinishSendAdminNotifyPost(trial bool, now int64) {
val := strconv.FormatInt(model.GetMillis(), 10) val := strconv.FormatInt(model.GetMillis(), 10)
sysVar := &model.System{Name: systemVarName, Value: val} sysVar := &model.System{Name: systemVarName, Value: val}
if err := a.Srv().Store().System().SaveOrUpdate(sysVar); err != nil { if err := a.Srv().Store().System().SaveOrUpdate(sysVar); err != nil {
mlog.Error("Unable to finish send admin notify post job", mlog.Err(err)) a.Log().Error("Unable to finish send admin notify post job", mlog.Err(err))
}
// All the requested features notifications are now sent in a post and can safely be removed except
// the plugin notify admin. We keep it as we do not want the same user to send the notification for the same plugin.
// We update the NotifyAdmin SentAt to keep track of it.
for pluginId := range pluginBasedData {
notifications := pluginBasedData[pluginId]
for _, notification := range notifications {
requiredFeature := notification.RequiredFeature
requiredPlan := notification.RequiredPlan
userId := notification.UserId
if err := a.Srv().Store().NotifyAdmin().Update(userId, requiredPlan, requiredFeature, now); err != nil {
a.Log().Error("Unable to update SentAt for work template feature", mlog.Err(err))
}
}
} }
// all the notifications are now sent in a post and can safely be removed
if err := a.Srv().Store().NotifyAdmin().DeleteBefore(trial, now); err != nil { if err := a.Srv().Store().NotifyAdmin().DeleteBefore(trial, now); err != nil {
mlog.Error("Unable to finish send admin notify post job", mlog.Err(err)) a.Log().Error("Unable to finish send admin notify post job", mlog.Err(err))
} }
mlog.Info("exit FinishSendAdminNotifyPost")
} }
func (a *App) groupNotifyAdminByUser(data []*model.NotifyAdminData) map[string][]*model.NotifyAdminData { func (a *App) groupNotifyAdminByUser(data []*model.NotifyAdminData) (map[string][]*model.NotifyAdminData, map[string][]*model.NotifyAdminData) {
myMap := make(map[string][]*model.NotifyAdminData) userBasedPaidFeatureData := make(map[string][]*model.NotifyAdminData)
userBasedPluginData := make(map[string][]*model.NotifyAdminData)
for _, d := range data { for _, d := range data {
myMap[d.UserId] = append(myMap[d.UserId], d) if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
userBasedPluginData[d.UserId] = append(userBasedPluginData[d.UserId], d)
} else {
userBasedPaidFeatureData[d.UserId] = append(userBasedPaidFeatureData[d.UserId], d)
}
} }
return userBasedPaidFeatureData, userBasedPluginData
return myMap
} }
func (a *App) groupNotifyAdminByFeature(data []*model.NotifyAdminData) map[model.MattermostPaidFeature][]*model.NotifyAdminData { func (a *App) groupNotifyAdminByPaidFeature(data []*model.NotifyAdminData) map[model.MattermostFeature][]*model.NotifyAdminData {
myMap := make(map[model.MattermostPaidFeature][]*model.NotifyAdminData) myMap := make(map[model.MattermostFeature][]*model.NotifyAdminData)
for _, d := range data { for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
continue
}
myMap[d.RequiredFeature] = append(myMap[d.RequiredFeature], d) myMap[d.RequiredFeature] = append(myMap[d.RequiredFeature], d)
} }
return myMap
}
func (a *App) groupNotifyAdminByPlugin(data []*model.NotifyAdminData) map[string][]*model.NotifyAdminData {
myMap := make(map[string][]*model.NotifyAdminData)
for _, d := range data {
if strings.HasPrefix(string(d.RequiredFeature), string(model.PluginFeature)) {
plugins := strings.Split(d.RequiredPlan, ",")
for _, plugin := range plugins {
myMap[plugin] = append(myMap[plugin], d)
}
}
}
return myMap return myMap
} }

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

@@ -15,9 +15,11 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const PluginIdGithub = "github"
func Test_SendNotifyAdminPosts(t *testing.T) { func Test_SendNotifyAdminPosts(t *testing.T) {
t.Run("no error sending upgrade post when no notifications are available", func(t *testing.T) { t.Run("no error sending non trial upgrade post when no notifications are available", func(t *testing.T) {
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()
@@ -28,7 +30,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
}) })
t.Run("no error sending trial post when do notifications are available", func(t *testing.T) { t.Run("no error sending trial upgrade post when no notifications are available", func(t *testing.T) {
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()
@@ -45,7 +47,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud")) th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
// some some notifications // some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id, UserId: th.BasicUser.Id,
RequiredPlan: model.LicenseShortSkuProfessional, RequiredPlan: model.LicenseShortSkuProfessional,
@@ -93,13 +95,13 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
require.Equal(t, "2 members of the test workspace have requested a workspace upgrade for: ", post.Message) require.Equal(t, "2 members of the test workspace have requested a workspace upgrade for: ", post.Message)
}) })
t.Run("successfully send trial notification", func(t *testing.T) { t.Run("successfully send trial upgrade notification", func(t *testing.T) {
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud")) th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
// some some notifications // some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id, UserId: th.BasicUser.Id,
RequiredPlan: model.LicenseShortSkuProfessional, RequiredPlan: model.LicenseShortSkuProfessional,
@@ -141,13 +143,128 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
require.Equal(t, "1 member of the test workspace has requested starting the Enterprise trial for access to: ", post.Message) require.Equal(t, "1 member of the test workspace has requested starting the Enterprise trial for access to: ", post.Message)
}) })
t.Run("error when trying to send post before end of cool off period", func(t *testing.T) { t.Run("successfully send install plugin notification", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
// some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id,
RequiredPlan: PluginIdGithub,
RequiredFeature: model.PluginFeature,
Trial: false,
})
require.Nil(t, appErr)
ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil)
appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false)
require.Nil(t, appErr)
bot, appErr := th.App.GetSystemBot()
require.Nil(t, appErr)
var channel *model.Channel
var err error
var timeout = 5 * time.Second
begin := time.Now()
for {
if time.Since(begin) > timeout {
break
}
channel, err = th.App.Srv().Store().Channel().GetByName("", model.GetDMNameFromIds(bot.UserId, th.SystemAdminUser.Id), false)
if err == nil && channel != nil {
break
}
time.Sleep(100 * time.Millisecond)
}
require.NoError(t, err, "Expected message to have been sent within %d seconds", timeout)
postList, err := th.App.Srv().Store().Post().GetPosts(model.GetPostsOptions{ChannelId: channel.Id, Page: 0, PerPage: 1}, false, map[string]bool{})
require.NoError(t, err)
post := postList.Posts[postList.Order[0]]
require.Equal(t, fmt.Sprintf("%spl_notification", model.PostCustomTypePrefix), post.Type)
require.Equal(t, bot.UserId, post.UserId)
})
t.Run("persist notify admin data after sending the install plugin notification", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
// some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id,
RequiredPlan: PluginIdGithub,
RequiredFeature: model.PluginFeature,
Trial: false,
})
require.Nil(t, appErr)
ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil)
appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false)
require.Nil(t, appErr)
bot, appErr := th.App.GetSystemBot()
require.Nil(t, appErr)
var channel *model.Channel
var err error
var timeout = 5 * time.Second
begin := time.Now()
for {
if time.Since(begin) > timeout {
break
}
channel, err = th.App.Srv().Store().Channel().GetByName("", model.GetDMNameFromIds(bot.UserId, th.SystemAdminUser.Id), false)
if err == nil && channel != nil {
break
}
time.Sleep(100 * time.Millisecond)
}
require.NoError(t, err, "Expected message to have been sent within %d seconds", timeout)
postList, err := th.App.Srv().Store().Post().GetPosts(model.GetPostsOptions{ChannelId: channel.Id, Page: 0, PerPage: 1}, false, map[string]bool{})
require.NoError(t, err)
post := postList.Posts[postList.Order[0]]
require.Equal(t, fmt.Sprintf("%spl_notification", model.PostCustomTypePrefix), post.Type)
require.Equal(t, bot.UserId, post.UserId)
data, err := th.App.Srv().Store().NotifyAdmin().GetDataByUserIdAndFeature(th.BasicUser.Id, model.PluginFeature)
require.NoError(t, err)
require.Equal(t, len(data), 1)
})
t.Run("error sending more than one notification to the same user and for the same plugin", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
err := th.App.SaveAdminNotification(th.BasicUser.Id, &model.NotifyAdminToUpgradeRequest{
RequiredPlan: PluginIdGithub,
RequiredFeature: model.PluginFeature,
TrialNotification: false,
})
require.Nil(t, err)
err = th.App.SaveAdminNotification(th.BasicUser.Id, &model.NotifyAdminToUpgradeRequest{
RequiredPlan: PluginIdGithub,
RequiredFeature: model.PluginFeature,
TrialNotification: false,
})
require.Equal(t, err.Error(), "app.SaveAdminNotification: Already notified admin")
})
t.Run("error when trying to send upgrade post before end of cool off period", func(t *testing.T) {
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense("cloud")) th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
// some some notifications // some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id, UserId: th.BasicUser.Id,
RequiredPlan: model.LicenseShortSkuProfessional, RequiredPlan: model.LicenseShortSkuProfessional,
@@ -173,7 +290,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
require.Equal(t, appErr.Error(), "SendNotifyAdminPosts: Unable to send notification post., Cannot notify yet") require.Equal(t, appErr.Error(), "SendNotifyAdminPosts: Unable to send notification post., Cannot notify yet")
}) })
t.Run("can send post at the end of cool off period", func(t *testing.T) { t.Run("can send upgrade post at the end of cool off period", func(t *testing.T) {
th := Setup(t).InitBasic() th := Setup(t).InitBasic()
defer th.TearDown() defer th.TearDown()
@@ -182,7 +299,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
os.Setenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS", "0.00003472222222") // set to 3 seconds os.Setenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS", "0.00003472222222") // set to 3 seconds
defer os.Unsetenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS") defer os.Unsetenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS")
// some some notifications // some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id, UserId: th.BasicUser.Id,
RequiredPlan: model.LicenseShortSkuProfessional, RequiredPlan: model.LicenseShortSkuProfessional,
@@ -215,7 +332,7 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
th.App.Srv().SetLicense(model.NewTestLicense("cloud")) th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
// some some notifications // some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{ _, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id, UserId: th.BasicUser.Id,
RequiredPlan: model.LicenseShortSkuProfessional, RequiredPlan: model.LicenseShortSkuProfessional,
@@ -264,4 +381,68 @@ func Test_SendNotifyAdminPosts(t *testing.T) {
require.Equal(t, bot.UserId, post.UserId) require.Equal(t, bot.UserId, post.UserId)
require.Equal(t, "1 member of the test workspace has requested a workspace upgrade for: ", post.Message) // expect only one member's notification even though 2 were added require.Equal(t, "1 member of the test workspace has requested a workspace upgrade for: ", post.Message) // expect only one member's notification even though 2 were added
}) })
t.Run("correctly send upgrade and install plugin post with the correct user request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
os.Setenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS", "0")
defer os.Unsetenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS")
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
ctx := request.NewContext(context.Background(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.NewId(), model.Session{}, nil)
// some notifications
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id,
RequiredPlan: model.LicenseShortSkuProfessional,
RequiredFeature: model.PaidFeatureGuestAccounts,
Trial: false,
})
require.Nil(t, appErr)
appErr = th.App.SendNotifyAdminPosts(ctx, "test", "", false)
require.Nil(t, appErr)
// some notifications
_, appErr = th.App.SaveAdminNotifyData(&model.NotifyAdminData{
UserId: th.BasicUser.Id,
RequiredPlan: PluginIdGithub,
RequiredFeature: model.PluginFeature,
Trial: false,
})
require.Nil(t, appErr)
appErr = th.App.SendNotifyAdminPosts(ctx, "test", "", false)
require.Nil(t, appErr)
bot, appErr := th.App.GetSystemBot()
require.Nil(t, appErr)
var channel *model.Channel
var err error
var timeout = 5 * time.Second
begin := time.Now()
for {
if time.Since(begin) > timeout {
break
}
channel, err = th.App.Srv().Store().Channel().GetByName("", model.GetDMNameFromIds(bot.UserId, th.SystemAdminUser.Id), false)
if err == nil && channel != nil {
break
}
time.Sleep(100 * time.Millisecond)
}
require.NoError(t, err, "Expected message to have been sent within %d seconds", timeout)
postList, err := th.App.Srv().Store().Post().GetPosts(model.GetPostsOptions{ChannelId: channel.Id, Page: 0, PerPage: 2}, false, map[string]bool{})
require.NoError(t, err)
installPluginPost := postList.Posts[postList.Order[0]]
require.Equal(t, fmt.Sprintf("%spl_notification", model.PostCustomTypePrefix), installPluginPost.Type)
require.Equal(t, bot.UserId, installPluginPost.UserId)
upgradePost := postList.Posts[postList.Order[1]]
require.Equal(t, fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), upgradePost.Type)
require.Equal(t, bot.UserId, upgradePost.UserId)
require.Equal(t, "1 member of the test workspace has requested a workspace upgrade for: ", upgradePost.Message)
})
} }

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

@@ -4430,7 +4430,7 @@ func (a *OpenTracingAppLayer) FindTeamByName(name string) bool {
return resultVar0 return resultVar0
} }
func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64) { func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64, pluginBasedData map[string][]*model.NotifyAdminData) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FinishSendAdminNotifyPost") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FinishSendAdminNotifyPost")
@@ -4442,7 +4442,7 @@ func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64) {
}() }()
defer span.Finish() defer span.Finish()
a.app.FinishSendAdminNotifyPost(trial, now) a.app.FinishSendAdminNotifyPost(trial, now, pluginBasedData)
} }
func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) { func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
@@ -18521,7 +18521,7 @@ func (a *OpenTracingAppLayer) UpsertGroupSyncable(groupSyncable *model.GroupSync
return resultVar0, resultVar1 return resultVar0, resultVar1
} }
func (a *OpenTracingAppLayer) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostPaidFeature) bool { func (a *OpenTracingAppLayer) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostFeature) bool {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserAlreadyNotifiedOnRequiredFeature") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserAlreadyNotifiedOnRequiredFeature")

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

@@ -1543,6 +1543,12 @@ func (s *Server) initJobs() {
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeTrialNotifyAdmin), notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeTrialNotifyAdmin),
) )
s.Jobs.RegisterJobType(
model.JobTypeInstallPluginNotifyAdmin,
notify_admin.MakeInstallPluginNotifyWorker(s.Jobs, New(ServerConnector(s.Channels()))),
notify_admin.MakeInstallPluginScheduler(s.Jobs, s.License(), model.JobTypeInstallPluginNotifyAdmin),
)
s.Jobs.RegisterJobType( s.Jobs.RegisterJobType(
model.JobTypeHostedPurchaseScreening, model.JobTypeHostedPurchaseScreening,
hosted_purchase_screening.MakeWorker(s.Jobs, s.License(), s.Store().System()), hosted_purchase_screening.MakeWorker(s.Jobs, s.License(), s.Store().System()),

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

@@ -204,6 +204,10 @@ db/migrations/mysql/000101_create_true_up_review_history.down.sql
db/migrations/mysql/000101_create_true_up_review_history.up.sql db/migrations/mysql/000101_create_true_up_review_history.up.sql
db/migrations/mysql/000102_posts_originalid_index.down.sql db/migrations/mysql/000102_posts_originalid_index.down.sql
db/migrations/mysql/000102_posts_originalid_index.up.sql db/migrations/mysql/000102_posts_originalid_index.up.sql
db/migrations/mysql/000103_add_sentat_to_notifyadmin.down.sql
db/migrations/mysql/000103_add_sentat_to_notifyadmin.up.sql
db/migrations/mysql/000104_upgrade_notifyadmin.down.sql
db/migrations/mysql/000104_upgrade_notifyadmin.up.sql
db/migrations/postgres/000001_create_teams.down.sql db/migrations/postgres/000001_create_teams.down.sql
db/migrations/postgres/000001_create_teams.up.sql db/migrations/postgres/000001_create_teams.up.sql
db/migrations/postgres/000002_create_team_members.down.sql db/migrations/postgres/000002_create_team_members.down.sql
@@ -408,3 +412,7 @@ db/migrations/postgres/000101_create_true_up_review_history.down.sql
db/migrations/postgres/000101_create_true_up_review_history.up.sql db/migrations/postgres/000101_create_true_up_review_history.up.sql
db/migrations/postgres/000102_posts_originalid_index.down.sql db/migrations/postgres/000102_posts_originalid_index.down.sql
db/migrations/postgres/000102_posts_originalid_index.up.sql db/migrations/postgres/000102_posts_originalid_index.up.sql
db/migrations/postgres/000103_add_sentat_to_notifyadmin.down.sql
db/migrations/postgres/000103_add_sentat_to_notifyadmin.up.sql
db/migrations/postgres/000104_upgrade_notifyadmin.down.sql
db/migrations/postgres/000104_upgrade_notifyadmin.up.sql

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

@@ -0,0 +1,14 @@
SET @preparedStatement = (SELECT IF(
EXISTS(
SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
WHERE table_name = 'NotifyAdmin'
AND table_schema = DATABASE()
AND column_name = 'SentAt'
) > 0,
'ALTER TABLE NotifyAdmin DROP COLUMN SentAt;',
'SELECT 1;'
));
PREPARE removeColumnIfExists FROM @preparedStatement;
EXECUTE removeColumnIfExists;
DEALLOCATE PREPARE removeColumnIfExists;

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

@@ -0,0 +1,14 @@
SET @preparedStatement = (SELECT IF(
NOT EXISTS(
SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'NotifyAdmin'
AND table_schema = DATABASE()
AND column_name = 'SentAt'
),
'ALTER TABLE NotifyAdmin ADD COLUMN SentAt bigint DEFAULT NULL;',
'SELECT 1;'
));
PREPARE addColumnIfNotExists FROM @preparedStatement;
EXECUTE addColumnIfNotExists;
DEALLOCATE PREPARE addColumnIfNotExists;

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

@@ -0,0 +1,29 @@
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'NotifyAdmin'
AND table_schema = DATABASE()
AND column_name = 'RequiredFeature'
AND column_type != 'varchar(100)'
) > 0,
'ALTER TABLE NotifyAdmin MODIFY COLUMN RequiredFeature varchar(100);',
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'NotifyAdmin'
AND table_schema = DATABASE()
AND column_name = 'RequiredPlan'
AND column_type != 'varchar(26)'
) > 0,
'ALTER TABLE NotifyAdmin MODIFY COLUMN RequiredPlan varchar(26);',
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;

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

@@ -0,0 +1,29 @@
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'NotifyAdmin'
AND table_schema = DATABASE()
AND column_name = 'RequiredFeature'
AND column_type != 'varchar(255)'
) > 0,
'ALTER TABLE NotifyAdmin MODIFY COLUMN RequiredFeature varchar(255);',
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'NotifyAdmin'
AND table_schema = DATABASE()
AND column_name = 'RequiredPlan'
AND column_type != 'varchar(100)'
) > 0,
'ALTER TABLE NotifyAdmin MODIFY COLUMN RequiredPlan varchar(100);',
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;

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

@@ -0,0 +1 @@
ALTER TABLE NotifyAdmin DROP COLUMN IF EXISTS SentAt;

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

@@ -0,0 +1 @@
ALTER TABLE NotifyAdmin ADD COLUMN IF NOT EXISTS SentAt bigint DEFAULT NULL;

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

@@ -0,0 +1,2 @@
ALTER TABLE NotifyAdmin ALTER COLUMN RequiredFeature TYPE VARCHAR(100);
ALTER TABLE NotifyAdmin ALTER COLUMN RequiredPlan TYPE VARCHAR(26);

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

@@ -0,0 +1,2 @@
ALTER TABLE NotifyAdmin ALTER COLUMN RequiredFeature TYPE VARCHAR(255);
ALTER TABLE NotifyAdmin ALTER COLUMN RequiredPlan TYPE VARCHAR(100);

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

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package notify_admin
import (
"strconv"
"time"
"github.com/mattermost/mattermost-server/v6/jobs"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
const installPluginSchedFreq = 1 * time.Minute
func MakeInstallPluginScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler {
isEnabled := func(cfg *model.Config) bool {
enabled := jobType == model.JobTypeInstallPluginNotifyAdmin
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType))
return enabled
}
return jobs.NewPeriodicScheduler(jobServer, jobType, installPluginSchedFreq, isEnabled)
}

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

@@ -11,6 +11,7 @@ import (
const ( const (
UpgradeNotifyJobName = "UpgradeNotifyAdmin" UpgradeNotifyJobName = "UpgradeNotifyAdmin"
TrialNotifyJobName = "TrialNotifyAdmin" TrialNotifyJobName = "TrialNotifyAdmin"
InstallNotifyJobName = "InstallNotifyAdmin"
) )
type AppIface interface { type AppIface interface {
@@ -52,3 +53,21 @@ func MakeTrialNotifyWorker(jobServer *jobs.JobServer, license *model.License, ap
worker := jobs.NewSimpleWorker(TrialNotifyJobName, jobServer, execute, isEnabled) worker := jobs.NewSimpleWorker(TrialNotifyJobName, jobServer, execute, isEnabled)
return worker return worker
} }
func MakeInstallPluginNotifyWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
isEnabled := func(_ *model.Config) bool {
return true
}
execute := func(job *model.Job) error {
defer jobServer.HandleJobPanic(job)
appErr := app.DoCheckForAdminNotifications(false)
if appErr != nil {
return appErr
}
return nil
}
worker := jobs.NewSimpleWorker(InstallNotifyJobName, jobServer, execute, isEnabled)
return worker
}

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

@@ -31,6 +31,7 @@ const (
JobTypeLastAccessibleFile = "last_accessible_file" JobTypeLastAccessibleFile = "last_accessible_file"
JobTypeUpgradeNotifyAdmin = "upgrade_notify_admin" JobTypeUpgradeNotifyAdmin = "upgrade_notify_admin"
JobTypeTrialNotifyAdmin = "trial_notify_admin" JobTypeTrialNotifyAdmin = "trial_notify_admin"
JobTypeInstallPluginNotifyAdmin = "install_plugin_notify_admin"
JobTypeHostedPurchaseScreening = "hosted_purchase_screening" JobTypeHostedPurchaseScreening = "hosted_purchase_screening"
JobStatusPending = "pending" JobStatusPending = "pending"

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

@@ -4,23 +4,26 @@
package model package model
import ( import (
"database/sql"
"fmt" "fmt"
"net/http" "net/http"
"strings"
) )
type MattermostPaidFeature string type MattermostFeature string
const ( const (
PaidFeatureGuestAccounts = MattermostPaidFeature("mattermost.feature.guest_accounts") PaidFeatureGuestAccounts = MattermostFeature("mattermost.feature.guest_accounts")
PaidFeatureCustomUsergroups = MattermostPaidFeature("mattermost.feature.custom_user_groups") PaidFeatureCustomUsergroups = MattermostFeature("mattermost.feature.custom_user_groups")
PaidFeatureCreateMultipleTeams = MattermostPaidFeature("mattermost.feature.create_multiple_teams") PaidFeatureCreateMultipleTeams = MattermostFeature("mattermost.feature.create_multiple_teams")
PaidFeatureStartcall = MattermostPaidFeature("mattermost.feature.start_call") PaidFeatureStartcall = MattermostFeature("mattermost.feature.start_call")
PaidFeaturePlaybooksRetrospective = MattermostPaidFeature("mattermost.feature.playbooks_retro") PaidFeaturePlaybooksRetrospective = MattermostFeature("mattermost.feature.playbooks_retro")
PaidFeatureUnlimitedMessages = MattermostPaidFeature("mattermost.feature.unlimited_messages") PaidFeatureUnlimitedMessages = MattermostFeature("mattermost.feature.unlimited_messages")
PaidFeatureUnlimitedFileStorage = MattermostPaidFeature("mattermost.feature.unlimited_file_storage") PaidFeatureUnlimitedFileStorage = MattermostFeature("mattermost.feature.unlimited_file_storage")
PaidFeatureAllProfessionalfeatures = MattermostPaidFeature("mattermost.feature.all_professional") PaidFeatureAllProfessionalfeatures = MattermostFeature("mattermost.feature.all_professional")
PaidFeatureAllEnterprisefeatures = MattermostPaidFeature("mattermost.feature.all_enterprise") PaidFeatureAllEnterprisefeatures = MattermostFeature("mattermost.feature.all_enterprise")
UpgradeDowngradedWorkspace = MattermostPaidFeature("mattermost.feature.upgrade_downgraded_workspace") UpgradeDowngradedWorkspace = MattermostFeature("mattermost.feature.upgrade_downgraded_workspace")
PluginFeature = MattermostFeature("mattermost.feature.plugin")
) )
var validSKUs map[string]struct{} = map[string]struct{}{ var validSKUs map[string]struct{} = map[string]struct{}{
@@ -29,7 +32,7 @@ var validSKUs map[string]struct{} = map[string]struct{}{
} }
// These are the features a non admin would typically ping an admin about // These are the features a non admin would typically ping an admin about
var paidFeatures map[MattermostPaidFeature]struct{} = map[MattermostPaidFeature]struct{}{ var paidFeatures map[MattermostFeature]struct{} = map[MattermostFeature]struct{}{
PaidFeatureGuestAccounts: {}, PaidFeatureGuestAccounts: {},
PaidFeatureCustomUsergroups: {}, PaidFeatureCustomUsergroups: {},
PaidFeatureCreateMultipleTeams: {}, PaidFeatureCreateMultipleTeams: {},
@@ -43,20 +46,24 @@ var paidFeatures map[MattermostPaidFeature]struct{} = map[MattermostPaidFeature]
} }
type NotifyAdminToUpgradeRequest struct { type NotifyAdminToUpgradeRequest struct {
TrialNotification bool `json:"trial_notification"` TrialNotification bool `json:"trial_notification"`
RequiredPlan string `json:"required_plan"` RequiredPlan string `json:"required_plan"`
RequiredFeature MattermostPaidFeature `json:"required_feature"` RequiredFeature MattermostFeature `json:"required_feature"`
} }
type NotifyAdminData struct { type NotifyAdminData struct {
CreateAt int64 `json:"create_at,omitempty"` CreateAt int64 `json:"create_at,omitempty"`
UserId string `json:"user_id"` UserId string `json:"user_id"`
RequiredPlan string `json:"required_plan"` RequiredPlan string `json:"required_plan"`
RequiredFeature MattermostPaidFeature `json:"required_feature"` RequiredFeature MattermostFeature `json:"required_feature"`
Trial bool `json:"trial"` Trial bool `json:"trial"`
SentAt sql.NullInt64 `json:"sent_at"`
} }
func (nad *NotifyAdminData) IsValid() *AppError { func (nad *NotifyAdminData) IsValid() *AppError {
if strings.HasPrefix(string(nad.RequiredFeature), string(PluginFeature)) {
return nil
}
if _, planOk := validSKUs[nad.RequiredPlan]; !planOk { if _, planOk := validSKUs[nad.RequiredPlan]; !planOk {
return NewAppError("NotifyAdmin.IsValid", fmt.Sprintf("Invalid plan, %s provided", nad.RequiredPlan), nil, "", http.StatusBadRequest) return NewAppError("NotifyAdmin.IsValid", fmt.Sprintf("Invalid plan, %s provided", nad.RequiredPlan), nil, "", http.StatusBadRequest)
} }

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

@@ -5249,7 +5249,7 @@ func (s *OpenTracingLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdmin
return result, err return result, err
} }
func (s *OpenTracingLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) { func (s *OpenTracingLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.GetDataByUserIdAndFeature") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.GetDataByUserIdAndFeature")
s.Root.Store.SetContext(newCtx) s.Root.Store.SetContext(newCtx)
@@ -5285,6 +5285,24 @@ func (s *OpenTracingLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*m
return result, err return result, err
} }
func (s *OpenTracingLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Update")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerOAuthStore) DeleteApp(id string) error { func (s *OpenTracingLayerOAuthStore) DeleteApp(id string) error {
origCtx := s.Root.Store.Context() origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.DeleteApp") span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.DeleteApp")

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

@@ -5941,7 +5941,7 @@ func (s *RetryLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData,
} }
func (s *RetryLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) { func (s *RetryLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
tries := 0 tries := 0
for { for {
@@ -5983,6 +5983,27 @@ func (s *RetryLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.N
} }
func (s *RetryLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
tries := 0
for {
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerOAuthStore) DeleteApp(id string) error { func (s *RetryLayerOAuthStore) DeleteApp(id string) error {
tries := 0 tries := 0

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

@@ -42,7 +42,7 @@ func (s SqlNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdm
return data, nil return data, nil
} }
func (s SqlNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) { func (s SqlNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
data := []*model.NotifyAdminData{} data := []*model.NotifyAdminData{}
query, args, err := s.getQueryBuilder(). query, args, err := s.getQueryBuilder().
Select("*"). Select("*").
@@ -50,7 +50,7 @@ func (s SqlNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature mo
Where(sq.Eq{"UserId": userId, "RequiredFeature": feature}). Where(sq.Eq{"UserId": userId, "RequiredFeature": feature}).
ToSql() ToSql()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all notifcation data by user id and required feature") return nil, errors.Wrap(err, "could not build sql query to get all notification data by user id and required feature")
} }
if err := s.GetReplicaX().Select(&data, query, args...); err != nil { if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
@@ -67,7 +67,8 @@ func (s SqlNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
query, args, err := s.getQueryBuilder(). query, args, err := s.getQueryBuilder().
Select("*"). Select("*").
From("NotifyAdmin"). From("NotifyAdmin").
Where(sq.Eq{"trial": trial}). Where(sq.Eq{"Trial": trial}).
Where("(SentAt IS NULL)").
ToSql() ToSql()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not build sql query to get all notifcation data") return nil, errors.Wrap(err, "could not build sql query to get all notifcation data")
@@ -80,8 +81,15 @@ func (s SqlNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
} }
func (s SqlNotifyAdminStore) DeleteBefore(trial bool, now int64) error { func (s SqlNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
if _, err := s.GetMasterX().Exec("DELETE FROM NotifyAdmin WHERE trial = ? AND createat < ?", trial, now); err != nil { if _, err := s.GetMasterX().Exec("DELETE FROM NotifyAdmin WHERE Trial = ? AND CreateAt < ? AND SentAt IS NULL", trial, now); err != nil {
return errors.Wrapf(err, "failed to remove all notification data with trial=%t", trial) return errors.Wrapf(err, "failed to remove all notification data with trial=%t", trial)
} }
return nil return nil
} }
func (s SqlNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
if _, err := s.GetMasterX().Exec("UPDATE NotifyAdmin SET SentAt = ? WHERE UserId = ? AND RequiredPlan = ? AND RequiredFeature = ?", now, userId, requiredPlan, requiredFeature); err != nil {
return errors.Wrapf(err, "failed to update SentAt for userId=%s and requiredPlan=%s", userId, requiredPlan)
}
return nil
}

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

@@ -942,9 +942,10 @@ type LinkMetadataStore interface {
type NotifyAdminStore interface { type NotifyAdminStore interface {
Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error)
GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error)
Get(trial bool) ([]*model.NotifyAdminData, error) Get(trial bool) ([]*model.NotifyAdminData, error)
DeleteBefore(trial bool, now int64) error DeleteBefore(trial bool, now int64) error
Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error
} }
type SharedChannelStore interface { type SharedChannelStore interface {

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

@@ -52,11 +52,11 @@ func (_m *NotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
} }
// GetDataByUserIdAndFeature provides a mock function with given fields: userId, feature // GetDataByUserIdAndFeature provides a mock function with given fields: userId, feature
func (_m *NotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) { func (_m *NotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
ret := _m.Called(userId, feature) ret := _m.Called(userId, feature)
var r0 []*model.NotifyAdminData var r0 []*model.NotifyAdminData
if rf, ok := ret.Get(0).(func(string, model.MattermostPaidFeature) []*model.NotifyAdminData); ok { if rf, ok := ret.Get(0).(func(string, model.MattermostFeature) []*model.NotifyAdminData); ok {
r0 = rf(userId, feature) r0 = rf(userId, feature)
} else { } else {
if ret.Get(0) != nil { if ret.Get(0) != nil {
@@ -65,7 +65,7 @@ func (_m *NotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature mod
} }
var r1 error var r1 error
if rf, ok := ret.Get(1).(func(string, model.MattermostPaidFeature) error); ok { if rf, ok := ret.Get(1).(func(string, model.MattermostFeature) error); ok {
r1 = rf(userId, feature) r1 = rf(userId, feature)
} else { } else {
r1 = ret.Error(1) r1 = ret.Error(1)
@@ -96,3 +96,17 @@ func (_m *NotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdmi
return r0, r1 return r0, r1
} }
// Update provides a mock function with given fields: userId, requiredPlan, requiredFeature, now
func (_m *NotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
ret := _m.Called(userId, requiredPlan, requiredFeature, now)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, model.MattermostFeature, int64) error); ok {
r0 = rf(userId, requiredPlan, requiredFeature, now)
} else {
r0 = ret.Error(0)
}
return r0
}

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

@@ -4,6 +4,7 @@
package storetest package storetest
import ( import (
"database/sql"
"testing" "testing"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/model"
@@ -11,11 +12,14 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const PluginIdJenkins = "jenkins"
func TestNotifyAdminStore(t *testing.T, ss store.Store) { func TestNotifyAdminStore(t *testing.T, ss store.Store) {
t.Run("Save", func(t *testing.T) { testNotifyAdminStoreSave(t, ss) }) t.Run("Save", func(t *testing.T) { testNotifyAdminStoreSave(t, ss) })
t.Run("testGetDataByUserIdAndFeature", func(t *testing.T) { testGetDataByUserIdAndFeature(t, ss) }) t.Run("testGetDataByUserIdAndFeature", func(t *testing.T) { testGetDataByUserIdAndFeature(t, ss) })
t.Run("testGet", func(t *testing.T) { testGet(t, ss) }) t.Run("testGet", func(t *testing.T) { testGet(t, ss) })
t.Run("testDeleteBefore", func(t *testing.T) { testDeleteBefore(t, ss) }) t.Run("testDeleteBefore", func(t *testing.T) { testDeleteBefore(t, ss) })
t.Run("testUpdate", func(t *testing.T) { testUpdate(t, ss) })
} }
func tearDown(t *testing.T, ss store.Store) { func tearDown(t *testing.T, ss store.Store) {
@@ -145,6 +149,28 @@ func testGetDataByUserIdAndFeature(t *testing.T, ss store.Store) {
tearDown(t, ss) tearDown(t, ss)
} }
func testUpdate(t *testing.T, ss store.Store) {
userId1 := model.NewId()
d1 := &model.NotifyAdminData{
UserId: userId1,
RequiredPlan: PluginIdJenkins,
RequiredFeature: model.PluginFeature,
}
_, err := ss.NotifyAdmin().Save(d1)
require.NoError(t, err)
err = ss.NotifyAdmin().Update(d1.UserId, d1.RequiredPlan, d1.RequiredFeature, 100)
require.NoError(t, err)
userRequest, err := ss.NotifyAdmin().GetDataByUserIdAndFeature(d1.UserId, d1.RequiredFeature)
require.NoError(t, err)
require.Equal(t, len(userRequest), 1)
require.Equal(t, userRequest[0].SentAt, sql.NullInt64{Int64: 100, Valid: true})
tearDown(t, ss)
}
func testDeleteBefore(t *testing.T, ss store.Store) { func testDeleteBefore(t *testing.T, ss store.Store) {
userId1 := model.NewId() userId1 := model.NewId()
d1 := &model.NotifyAdminData{ d1 := &model.NotifyAdminData{

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

@@ -4764,7 +4764,7 @@ func (s *TimerLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData,
return result, err return result, err
} }
func (s *TimerLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) { func (s *TimerLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostFeature) ([]*model.NotifyAdminData, error) {
start := time.Now() start := time.Now()
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature) result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
@@ -4796,6 +4796,22 @@ func (s *TimerLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.N
return result, err return result, err
} }
func (s *TimerLayerNotifyAdminStore) Update(userId string, requiredPlan string, requiredFeature model.MattermostFeature, now int64) error {
start := time.Now()
err := s.NotifyAdminStore.Update(userId, requiredPlan, requiredFeature, now)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("NotifyAdminStore.Update", success, elapsed)
}
return err
}
func (s *TimerLayerOAuthStore) DeleteApp(id string) error { func (s *TimerLayerOAuthStore) DeleteApp(id string) error {
start := time.Now() start := time.Now()