[MM-45564] - Notify Admin v2 (#20777)
* [MM-45564] - Notify Admin v2 * add dummy data * update dummy data * add store methods * experiment with recurring task * complete saving of the notification * make improvements * make improvements * add store layer tests * fix lint * update store layer tests * add app layer unit tests * add store layers * add app layer tests * fix lint * fix lint * fix tests * fix tests lint * fix lint * fix lint * fix retry layer test * add notifications manual trigger * filter notifications based on current plan * add test case * temp change * feedback impl * fix translations * change job scheduler * refactor job * fix store layer tests * extract i18n * fix lint * fix translations * fix translations * add license statement for new file * feedback impl-2 * fix lint * update make file * add intl ids * improve * fix lint * feedback impl * move code and rename files * fix lint * feedback impl * add config for trigger notifications api * fix tests * tmp change * undo temp changes Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c11ad8995f
Коммит
bd42f0cd8c
@@ -438,6 +438,7 @@ type AppIface interface {
|
||||
BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError
|
||||
BulkImport(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int) (*model.AppError, int)
|
||||
BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int)
|
||||
CanNotifyAdmin(trial bool) bool
|
||||
CancelJob(jobId string) *model.AppError
|
||||
ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError)
|
||||
Channels() *Channels
|
||||
@@ -540,6 +541,7 @@ type AppIface interface {
|
||||
DisableAutoResponder(c request.CTX, userID string, asAdmin bool) *model.AppError
|
||||
DisableUserAccessToken(token *model.UserAccessToken) *model.AppError
|
||||
DoAppMigrations()
|
||||
DoCheckForAdminNotifications(trial bool) *model.AppError
|
||||
DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError)
|
||||
DoEmojisPermissionsMigration()
|
||||
DoGuestRolesCreationMigration()
|
||||
@@ -564,6 +566,7 @@ type AppIface interface {
|
||||
FillInChannelsProps(c request.CTX, channelList model.ChannelList) *model.AppError
|
||||
FilterUsersByVisible(viewer *model.User, otherUsers []*model.User) ([]*model.User, *model.AppError)
|
||||
FindTeamByName(name string) bool
|
||||
FinishSendAdminNotifyPost(trial bool, now int64)
|
||||
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
|
||||
GeneratePublicLink(siteURL string, info *model.FileInfo) string
|
||||
GenerateSupportPacket() []model.FileData
|
||||
@@ -907,7 +910,6 @@ type AppIface interface {
|
||||
NotificationsLog() *mlog.Logger
|
||||
NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError
|
||||
NotifySharedChannelUserUpdate(user *model.User)
|
||||
NotifySystemAdminsToUpgrade(c *request.Context, currentUserTeamID string) *model.AppError
|
||||
OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError
|
||||
OriginChecker() func(*http.Request) bool
|
||||
PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError)
|
||||
@@ -985,6 +987,8 @@ 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
|
||||
SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError
|
||||
SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError)
|
||||
SaveAndBroadcastStatus(status *model.Status)
|
||||
SaveBrandImage(imageData *multipart.FileHeader) *model.AppError
|
||||
SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError)
|
||||
@@ -1021,6 +1025,7 @@ type AppIface interface {
|
||||
SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError
|
||||
SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post
|
||||
SendNotifications(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error)
|
||||
SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError
|
||||
SendPasswordReset(email string, siteURL string) (bool, *model.AppError)
|
||||
SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError
|
||||
SendTestPushNotification(deviceID string) string
|
||||
@@ -1139,6 +1144,7 @@ type AppIface interface {
|
||||
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)
|
||||
UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostPaidFeature) bool
|
||||
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
|
||||
VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError
|
||||
VerifyUserEmail(userID, email string) *model.AppError
|
||||
|
||||
93
app/cloud.go
93
app/cloud.go
@@ -4,109 +4,16 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/product"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
func (a *App) NotifySystemAdminsToUpgrade(c *request.Context, currentUserTeamID string) *model.AppError {
|
||||
userId := c.Session().Id
|
||||
|
||||
fakeId := strings.ReplaceAll(model.CloudNotifyAdminInfo, "_", "") + "123456"
|
||||
|
||||
// check if already notified
|
||||
notificationPref, err := a.Srv().Store.Preference().Get(fakeId, model.PreferenceCloudUserEphemeralInfo, model.CloudNotifyAdminInfo)
|
||||
if err != nil {
|
||||
mlog.Warn("Unable to get preference cloud_user_ephemeral_info", mlog.Err(err))
|
||||
}
|
||||
|
||||
if notificationPref != nil {
|
||||
info := &model.AdminNotificationUserInfo{}
|
||||
err = json.Unmarshal([]byte(notificationPref.Value), info)
|
||||
if err != nil {
|
||||
mlog.Warn("Unable to Unmarshal", mlog.Err(err))
|
||||
}
|
||||
|
||||
if !model.CanNotify(info.LastNotificationTimestamp) {
|
||||
return model.NewAppError("app.NotifySystemAdminsToUpgrade", "api.cloud.notify_admin_to_upgrade_error.already_notified", nil, "", http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
team, appErr := a.GetTeam(currentUserTeamID)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
sysadmins, appErr := a.GetUsersFromProfiles(&model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 100,
|
||||
Role: model.SystemAdminRoleId,
|
||||
Inactive: false,
|
||||
})
|
||||
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
systemBot, appErr := a.GetSystemBot()
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
for _, admin := range sysadmins {
|
||||
T := i18n.GetUserTranslations(admin.Locale)
|
||||
channel, appErr := a.GetOrCreateDirectChannel(c, systemBot.UserId, admin.Id)
|
||||
if appErr != nil {
|
||||
mlog.Warn("Error getting direct channel", mlog.Err(appErr))
|
||||
continue
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
Message: T("api.cloud.upgrade_plan_bot_message", map[string]any{"TeamName": team.Name}),
|
||||
UserId: systemBot.UserId,
|
||||
ChannelId: channel.Id,
|
||||
Type: fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), // webapp will have to create renderer for this custom post type
|
||||
}
|
||||
|
||||
_, appErr = a.CreatePost(c, post, channel, false, true)
|
||||
if appErr != nil {
|
||||
mlog.Warn("Error creating post", mlog.Err(appErr))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// mark as done for current user until end of cool off period
|
||||
out, err := json.Marshal(&model.AdminNotificationUserInfo{
|
||||
LastUserIDToNotify: userId,
|
||||
LastNotificationTimestamp: model.GetMillis(),
|
||||
})
|
||||
if err != nil {
|
||||
mlog.Warn("Unable to Marshal", mlog.Err(err))
|
||||
}
|
||||
|
||||
pref := model.Preference{
|
||||
UserId: fakeId, // to only have one preference for now and not a preference per user
|
||||
Category: model.PreferenceCloudUserEphemeralInfo,
|
||||
Name: model.CloudNotifyAdminInfo,
|
||||
Value: string(out),
|
||||
}
|
||||
|
||||
if err := a.Srv().Store.Preference().Save(model.Preferences{pref}); err != nil {
|
||||
mlog.Warn("Encountered error saving cloud_user_ephemeral_info preference", mlog.Err(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure cloud service wrapper implements `product.CloudService`
|
||||
var _ product.CloudService = (*cloudWrapper)(nil)
|
||||
|
||||
|
||||
243
app/notify_admin.go
Обычный файл
243
app/notify_admin.go
Обычный файл
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"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/store"
|
||||
)
|
||||
|
||||
const lastTrialNotificationTimeStamp = "LAST_TRIAL_NOTIFICATION_TIMESTAMP"
|
||||
const lastUpgradeNotificationTimeStamp = "LAST_UPGRADE_NOTIFICATION_TIMESTAMP"
|
||||
const defaultNotifyAdminCoolOffDays = 14
|
||||
|
||||
func (a *App) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
|
||||
requiredFeature := notifyData.RequiredFeature
|
||||
requiredPlan := notifyData.RequiredPlan
|
||||
trial := notifyData.TrialNotification
|
||||
|
||||
if a.UserAlreadyNotifiedOnRequiredFeature(userId, requiredFeature) {
|
||||
return model.NewAppError("app.SaveAdminNotification", "api.cloud.notify_admin_to_upgrade_error.already_notified", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
_, appErr := a.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: userId,
|
||||
RequiredPlan: requiredPlan,
|
||||
RequiredFeature: requiredFeature,
|
||||
Trial: trial,
|
||||
})
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) DoCheckForAdminNotifications(trial bool) *model.AppError {
|
||||
ctx := request.EmptyContext(a.Srv().Log())
|
||||
license := a.Srv().License()
|
||||
if license == nil {
|
||||
return model.NewAppError("DoCheckForAdminNotifications", "app.notify_admin.send_notification_post.app_error", nil, "No license found", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
currentSKU := license.SkuShortName
|
||||
workspaceName := ""
|
||||
|
||||
return a.SendNotifyAdminPosts(ctx, workspaceName, currentSKU, trial)
|
||||
}
|
||||
|
||||
func (a *App) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) {
|
||||
d, err := a.Srv().Store.NotifyAdmin().Save(data)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(err, &nfErr):
|
||||
return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, nfErr.Error(), http.StatusNotFound)
|
||||
default:
|
||||
return nil, model.NewAppError("SaveAdminNotifyData", "app.notify_admin.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func filterNotificationData(data []*model.NotifyAdminData, test func(*model.NotifyAdminData) bool) (ret []*model.NotifyAdminData) {
|
||||
for _, d := range data {
|
||||
if test(d) {
|
||||
ret = append(ret, d)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError {
|
||||
if !a.CanNotifyAdmin(trial) {
|
||||
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, "Cannot notify yet", http.StatusForbidden)
|
||||
}
|
||||
|
||||
sysadmins, appErr := a.GetUsersFromProfiles(&model.UserGetOptions{
|
||||
Page: 0,
|
||||
PerPage: 100,
|
||||
Role: model.SystemAdminRoleId,
|
||||
Inactive: false,
|
||||
})
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
systemBot, appErr := a.GetSystemBot()
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
now := model.GetMillis()
|
||||
|
||||
data, err := a.Srv().Store.NotifyAdmin().Get(trial)
|
||||
if err != nil {
|
||||
return model.NewAppError("SendNotifyAdminPosts", "app.notify_admin.send_notification_post.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
data = filterNotificationData(data, func(nad *model.NotifyAdminData) bool { return nad.RequiredPlan != currentSKU })
|
||||
|
||||
if len(data) == 0 {
|
||||
mlog.Warn("No notification data available")
|
||||
return nil
|
||||
}
|
||||
|
||||
userBasedData := a.groupNotifyAdminByUser(data)
|
||||
featureBasedData := a.groupNotifyAdminByFeature(data)
|
||||
props := make(model.StringInterface)
|
||||
|
||||
for _, admin := range sysadmins {
|
||||
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 {
|
||||
mlog.Warn("Error getting direct channel", mlog.Err(appErr))
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
mlog.Warn("Error creating post", mlog.Err(appErr))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
a.FinishSendAdminNotifyPost(trial, now)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostPaidFeature) bool {
|
||||
data, err := a.Srv().Store.NotifyAdmin().GetDataByUserIdAndFeature(user, feature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(data) > 0 {
|
||||
return true // if we find data, it means this user already notified on the need for this feature
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) CanNotifyAdmin(trial bool) bool {
|
||||
systemVarName := lastUpgradeNotificationTimeStamp
|
||||
if trial {
|
||||
systemVarName = lastTrialNotificationTimeStamp
|
||||
}
|
||||
|
||||
sysVal, sysValErr := a.Srv().Store.System().GetByName(systemVarName)
|
||||
if sysValErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
if errors.As(sysValErr, &nfErr) { // if no timestamps have been recorded before, system is free to notify
|
||||
return true
|
||||
}
|
||||
mlog.Error("Cannot notify", mlog.Err(sysValErr))
|
||||
return false
|
||||
}
|
||||
|
||||
lastNotificationTimestamp, err := strconv.ParseFloat(sysVal.Value, 64)
|
||||
if err != nil {
|
||||
mlog.Error("Cannot notify", mlog.Err(err))
|
||||
return false
|
||||
}
|
||||
|
||||
coolOffPeriodDaysEnv := os.Getenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS")
|
||||
coolOffPeriodDays, parseError := strconv.ParseFloat(coolOffPeriodDaysEnv, 64)
|
||||
if parseError != nil {
|
||||
coolOffPeriodDays = defaultNotifyAdminCoolOffDays
|
||||
}
|
||||
daysToMillis := coolOffPeriodDays * 24 * 60 * 60 * 1000
|
||||
timeDiff := model.GetMillis() - int64(lastNotificationTimestamp)
|
||||
return timeDiff >= int64(daysToMillis)
|
||||
}
|
||||
|
||||
func (a *App) FinishSendAdminNotifyPost(trial bool, now int64) {
|
||||
systemVarName := lastUpgradeNotificationTimeStamp
|
||||
if trial {
|
||||
systemVarName = lastTrialNotificationTimeStamp
|
||||
}
|
||||
|
||||
val := strconv.FormatInt(model.GetMillis(), 10)
|
||||
sysVar := &model.System{Name: systemVarName, Value: val}
|
||||
if err := a.Srv().Store.System().SaveOrUpdate(sysVar); err != nil {
|
||||
mlog.Error("Unable to finish send admin notify post job", 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 {
|
||||
mlog.Error("Unable to finish send admin notify post job", mlog.Err(err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (a *App) groupNotifyAdminByUser(data []*model.NotifyAdminData) map[string][]*model.NotifyAdminData {
|
||||
myMap := make(map[string][]*model.NotifyAdminData)
|
||||
for _, d := range data {
|
||||
myMap[d.UserId] = append(myMap[d.UserId], d)
|
||||
}
|
||||
|
||||
return myMap
|
||||
}
|
||||
|
||||
func (a *App) groupNotifyAdminByFeature(data []*model.NotifyAdminData) map[model.MattermostPaidFeature][]*model.NotifyAdminData {
|
||||
myMap := make(map[model.MattermostPaidFeature][]*model.NotifyAdminData)
|
||||
for _, d := range data {
|
||||
myMap[d.RequiredFeature] = append(myMap[d.RequiredFeature], d)
|
||||
}
|
||||
|
||||
return myMap
|
||||
}
|
||||
267
app/notify_admin_test.go
Обычный файл
267
app/notify_admin_test.go
Обычный файл
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_SendNotifyAdminPosts(t *testing.T) {
|
||||
|
||||
t.Run("no error sending upgrade post when no notifications are available", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
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)
|
||||
err := th.App.SendNotifyAdminPosts(ctx, "", "", false)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("no error sending trial post when do notifications are available", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
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)
|
||||
err := th.App.SendNotifyAdminPosts(ctx, "", "", true)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("successfully send upgrade notification", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
// some some notifications
|
||||
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureGuestAccounts,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
_, appErr = th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser2.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureGuestAccounts,
|
||||
})
|
||||
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, "test", "", false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
bot, appErr := th.App.GetSystemBot()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// message sending is async, wait time for it
|
||||
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("%sup_notification", model.PostCustomTypePrefix), post.Type)
|
||||
require.Equal(t, bot.UserId, post.UserId)
|
||||
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) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
// some some notifications
|
||||
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
Trial: true,
|
||||
})
|
||||
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, "test", "", true)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
bot, appErr := th.App.GetSystemBot()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// message sending is async, wait time for it
|
||||
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("%sup_notification", model.PostCustomTypePrefix), post.Type)
|
||||
require.Equal(t, bot.UserId, post.UserId)
|
||||
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) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
// some some notifications
|
||||
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
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)
|
||||
|
||||
// add some more notifications while in cool off
|
||||
_, appErr = th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureCustomUsergroups,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// second time trying to notify is forbidden
|
||||
appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false)
|
||||
require.NotNil(t, appErr)
|
||||
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) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
os.Setenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS", "0.00003472222222") // set to 3 seconds
|
||||
defer os.Unsetenv("MM_NOTIFY_ADMIN_COOL_OFF_DAYS")
|
||||
|
||||
// some some notifications
|
||||
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
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)
|
||||
|
||||
// add some more notifications while in cool off
|
||||
_, appErr = th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureCustomUsergroups,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// no error sending second time
|
||||
appErr = th.App.SendNotifyAdminPosts(ctx, "", "", false)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("can filter notifications when plan changes within cool off period", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
// some some notifications
|
||||
_, appErr := th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser.Id,
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
Trial: false,
|
||||
})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
_, appErr = th.App.SaveAdminNotifyData(&model.NotifyAdminData{
|
||||
UserId: th.BasicUser2.Id,
|
||||
RequiredPlan: model.LicenseShortSkuEnterprise,
|
||||
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
|
||||
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, "test", model.LicenseShortSkuProfessional, false) // try and send notification but workspace currentSKU has since changed to cloud-professional
|
||||
require.Nil(t, appErr)
|
||||
|
||||
bot, appErr := th.App.GetSystemBot()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// message sending is async, wait time for it
|
||||
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("%sup_notification", model.PostCustomTypePrefix), post.Type)
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -1083,6 +1083,23 @@ func (a *OpenTracingAppLayer) BulkImportWithPath(c *request.Context, jsonlReader
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CanNotifyAdmin(trial bool) bool {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CanNotifyAdmin")
|
||||
|
||||
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.CanNotifyAdmin(trial)
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) CancelJob(jobId string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CancelJob")
|
||||
@@ -3653,6 +3670,28 @@ func (a *OpenTracingAppLayer) DoAppMigrations() {
|
||||
a.app.DoAppMigrations()
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DoCheckForAdminNotifications(trial bool) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCheckForAdminNotifications")
|
||||
|
||||
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.DoCheckForAdminNotifications(trial)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCommandRequest")
|
||||
@@ -4362,6 +4401,21 @@ func (a *OpenTracingAppLayer) FindTeamByName(name string) bool {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) FinishSendAdminNotifyPost(trial bool, now int64) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FinishSendAdminNotifyPost")
|
||||
|
||||
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.FinishSendAdminNotifyPost(trial, now)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret")
|
||||
@@ -12485,28 +12539,6 @@ func (a *OpenTracingAppLayer) NotifySharedChannelUserUpdate(user *model.User) {
|
||||
a.app.NotifySharedChannelUserUpdate(user)
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) NotifySystemAdminsToUpgrade(c *request.Context, currentUserTeamID string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySystemAdminsToUpgrade")
|
||||
|
||||
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.NotifySystemAdminsToUpgrade(c, currentUserTeamID)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OpenInteractiveDialog")
|
||||
@@ -14331,6 +14363,50 @@ func (a *OpenTracingAppLayer) SanitizeTeams(session model.Session, teams []*mode
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotification")
|
||||
|
||||
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.SaveAdminNotification(userId, notifyData)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotifyData")
|
||||
|
||||
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.SaveAdminNotifyData(data)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SaveAndBroadcastStatus(status *model.Status) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAndBroadcastStatus")
|
||||
@@ -15189,6 +15265,28 @@ func (a *OpenTracingAppLayer) SendNotifications(c request.CTX, post *model.Post,
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendNotifyAdminPosts")
|
||||
|
||||
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.SendNotifyAdminPosts(c, workspaceName, currentSKU, trial)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPasswordReset")
|
||||
@@ -18003,6 +18101,23 @@ func (a *OpenTracingAppLayer) UpsertGroupSyncable(groupSyncable *model.GroupSync
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UserAlreadyNotifiedOnRequiredFeature(user string, feature model.MattermostPaidFeature) bool {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserAlreadyNotifiedOnRequiredFeature")
|
||||
|
||||
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.UserAlreadyNotifiedOnRequiredFeature(user, feature)
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserCanSeeOtherUser")
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/jobs/import_process"
|
||||
"github.com/mattermost/mattermost-server/v6/jobs/last_accessible_post"
|
||||
"github.com/mattermost/mattermost-server/v6/jobs/migrations"
|
||||
"github.com/mattermost/mattermost-server/v6/jobs/notify_admin"
|
||||
"github.com/mattermost/mattermost-server/v6/jobs/product_notices"
|
||||
"github.com/mattermost/mattermost-server/v6/jobs/resend_invitation_email"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
@@ -1846,6 +1847,18 @@ func (s *Server) initJobs() {
|
||||
last_accessible_post.MakeWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
|
||||
last_accessible_post.MakeScheduler(s.Jobs, s.License()),
|
||||
)
|
||||
|
||||
s.Jobs.RegisterJobType(
|
||||
model.JobTypeUpgradeNotifyAdmin,
|
||||
notify_admin.MakeUpgradeNotifyWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
|
||||
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeUpgradeNotifyAdmin),
|
||||
)
|
||||
|
||||
s.Jobs.RegisterJobType(
|
||||
model.JobTypeTrialNotifyAdmin,
|
||||
notify_admin.MakeTrialNotifyWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
|
||||
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeTrialNotifyAdmin),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) TelemetryId() string {
|
||||
|
||||
Ссылка в новой задаче
Block a user