[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
4
Makefile
4
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: build package run stop run-client run-server run-haserver stop-haserver stop-client stop-server restart restart-server restart-client restart-haserver start-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows package-prep package-linux package-osx package-windows internal-test-web-client vet run-server-for-web-client-tests diff-config prepackaged-plugins prepackaged-binaries test-server test-server-ee test-server-quick test-server-race migrations-bindata new-migration migrations-extract
|
.PHONY: build package run stop run-client run-server run-haserver stop-haserver stop-client stop-server restart restart-server restart-client restart-haserver start-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows package-prep package-linux package-osx package-windows internal-test-web-client vet run-server-for-web-client-tests diff-config prepackaged-plugins prepackaged-binaries test-server test-server-ee test-server-quick test-server-race new-migration migrations-extract
|
||||||
|
|
||||||
ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||||
|
|
||||||
@@ -357,8 +357,6 @@ new-migration: ## Creates a new migration. Run with make new-migration name=<>
|
|||||||
@echo "Generating new migration for postgres"
|
@echo "Generating new migration for postgres"
|
||||||
$(GOBIN)/morph generate $(name) --driver postgres --dir db/migrations --sequence
|
$(GOBIN)/morph generate $(name) --driver postgres --dir db/migrations --sequence
|
||||||
|
|
||||||
@echo "When you are done writing your migration, run 'make migrations-bindata'"
|
|
||||||
|
|
||||||
filestore-mocks: ## Creates mock files.
|
filestore-mocks: ## Creates mock files.
|
||||||
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
$(GO) install github.com/vektra/mockery/v2/...@v2.10.4
|
||||||
$(GOBIN)/mockery --dir shared/filestore --all --output shared/filestore/mocks --note 'Regenerate this file using `make filestore-mocks`.'
|
$(GOBIN)/mockery --dir shared/filestore --all --output shared/filestore/mocks --note 'Regenerate this file using `make filestore-mocks`.'
|
||||||
|
|||||||
@@ -50,25 +50,6 @@ func (api *API) InitCloud() {
|
|||||||
|
|
||||||
// POST /api/v4/cloud/webhook
|
// POST /api/v4/cloud/webhook
|
||||||
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
|
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
|
||||||
|
|
||||||
api.BaseRoutes.Cloud.Handle("/notify-admin-to-upgrade", api.APISessionRequired(handleNotifyAdminToUpgrade)).Methods("POST")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNotifyAdminToUpgrade(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
||||||
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
|
|
||||||
if err != nil {
|
|
||||||
c.SetInvalidParamWithErr("notifyAdminRequest", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
appErr := c.App.NotifySystemAdminsToUpgrade(c.AppContext, notifyAdminRequest.CurrentTeamId)
|
|
||||||
if appErr != nil {
|
|
||||||
c.Err = appErr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ReturnStatusOK(w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ package api4
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -296,109 +293,6 @@ func Test_requestTrial(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotifyAdminToUpgrade(t *testing.T) {
|
|
||||||
t.Run("user can only notify admin once in cool off period", func(t *testing.T) {
|
|
||||||
th := Setup(t).InitBasic().InitLogin()
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
statusCode := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
|
||||||
CurrentTeamId: th.BasicTeam.Id,
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
require.Equal(t, len(postList.Order), 1)
|
|
||||||
|
|
||||||
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, fmt.Sprintf("A member of %s has notified you to upgrade this workspace.", th.BasicTeam.Name), post.Message)
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, statusCode)
|
|
||||||
|
|
||||||
// second time trying to call notify endpoint by same user is forbidden
|
|
||||||
statusCode = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
|
||||||
CurrentTeamId: th.BasicTeam.Id,
|
|
||||||
})
|
|
||||||
require.Equal(t, http.StatusForbidden, statusCode)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("user can only notify admin after cool off period", func(t *testing.T) {
|
|
||||||
th := Setup(t).InitBasic().InitLogin()
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
os.Setenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS", "0.00003472222222") // set to 3 seconds
|
|
||||||
defer os.Unsetenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS")
|
|
||||||
|
|
||||||
statusCode := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
|
||||||
CurrentTeamId: th.BasicTeam.Id,
|
|
||||||
})
|
|
||||||
|
|
||||||
bot, appErr := th.App.GetSystemBot()
|
|
||||||
require.Nil(t, appErr)
|
|
||||||
|
|
||||||
channel, err := th.App.Srv().Store.Channel().GetByName("", model.GetDMNameFromIds(bot.UserId, th.SystemAdminUser.Id), false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
require.Equal(t, len(postList.Order), 1)
|
|
||||||
|
|
||||||
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, fmt.Sprintf("A member of %s has notified you to upgrade this workspace.", th.BasicTeam.Name), post.Message)
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, statusCode)
|
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
|
|
||||||
// second time trying to call notify endpoint by same user is NOT forbidden because it is after cool off period set to 3 seconds
|
|
||||||
statusCode = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
|
||||||
CurrentTeamId: th.BasicTeam.Id,
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, statusCode)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can cloud/model.Notify", func(t *testing.T) {
|
|
||||||
|
|
||||||
os.Setenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS", "10") // set to 10 days
|
|
||||||
canNotify := model.CanNotify(model.GetMillis())
|
|
||||||
require.Equal(t, false, canNotify)
|
|
||||||
|
|
||||||
os.Setenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS", "0.00003472222222") // set to 3 seconds
|
|
||||||
canNotify = model.CanNotify(model.GetMillis())
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
require.Equal(t, false, canNotify)
|
|
||||||
os.Unsetenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
func Test_validateBusinessEmail(t *testing.T) {
|
func Test_validateBusinessEmail(t *testing.T) {
|
||||||
t.Run("Returns forbidden for non admin executors", func(t *testing.T) {
|
t.Run("Returns forbidden for non admin executors", func(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
|
|||||||
57
api4/notify_admin.go
Обычный файл
57
api4/notify_admin.go
Обычный файл
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package api4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
|
||||||
|
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.SetInvalidParamWithErr("notifyAdminRequest", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := c.AppContext.Session().UserId
|
||||||
|
appErr := c.App.SaveAdminNotification(userId, notifyAdminRequest)
|
||||||
|
if appErr != nil {
|
||||||
|
c.Err = appErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
|
||||||
|
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
|
||||||
|
if err != nil {
|
||||||
|
c.SetInvalidParamWithErr("notifyAdminRequest", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// only system admins can manually trigger these notifications
|
||||||
|
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||||
|
c.SetPermissionError(model.PermissionManageSystem)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appErr := c.App.SendNotifyAdminPosts(c.AppContext, "", "", notifyAdminRequest.TrialNotification)
|
||||||
|
if appErr != nil {
|
||||||
|
c.Err = appErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
155
api4/notify_admin_test.go
Обычный файл
155
api4/notify_admin_test.go
Обычный файл
@@ -0,0 +1,155 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
package api4
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotifyAdmin(t *testing.T) {
|
||||||
|
t.Run("error when plan is unknown when notifying on upgrade", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: "Unknown plan",
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||||
|
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error when plan is unknown when notifying to trial", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: "Unknown plan",
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
TrialNotification: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||||
|
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error when feature is unknown when notifying on upgrade", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: "Unknown feature",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||||
|
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error when feature is unknown when notifying to trial", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: "Unknown feature",
|
||||||
|
TrialNotification: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||||
|
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error when user tries to notify again on same feature within the cool off period", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, statusCode)
|
||||||
|
|
||||||
|
// second attempt to notify for all professional features
|
||||||
|
statusCode, err = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, err.Error(), ": Already notified admin")
|
||||||
|
require.Equal(t, http.StatusForbidden, statusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("successfully save upgrade notification", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, statusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTriggerNotifyAdmin(t *testing.T) {
|
||||||
|
t.Run("error when EnableAPITriggerAdminNotifications is not true", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = false })
|
||||||
|
|
||||||
|
statusCode, err := th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, err.Error(), ": Internal error during cloud api request.")
|
||||||
|
require.Equal(t, http.StatusForbidden, statusCode)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error when non admins try to trigger notifications", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic().InitLogin()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
|
||||||
|
|
||||||
|
statusCode, err := th.Client.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, err.Error(), ": You do not have the appropriate permissions.")
|
||||||
|
require.Equal(t, http.StatusForbidden, statusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("happy path", func(t *testing.T) {
|
||||||
|
th := Setup(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
|
||||||
|
|
||||||
|
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, statusCode)
|
||||||
|
|
||||||
|
statusCode, err = th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, statusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -103,6 +103,9 @@ func (api *API) InitUser() {
|
|||||||
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods("DELETE")
|
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods("DELETE")
|
||||||
api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.APISessionRequired(updateReadStateThreadByUser)).Methods("PUT")
|
api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.APISessionRequired(updateReadStateThreadByUser)).Methods("PUT")
|
||||||
api.BaseRoutes.UserThread.Handle("/set_unread/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(setUnreadThreadByPostId)).Methods("POST")
|
api.BaseRoutes.UserThread.Handle("/set_unread/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(setUnreadThreadByPostId)).Methods("POST")
|
||||||
|
|
||||||
|
api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods("POST")
|
||||||
|
api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ type AppIface interface {
|
|||||||
BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError
|
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)
|
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)
|
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
|
CancelJob(jobId string) *model.AppError
|
||||||
ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError)
|
ChannelMembersToRemove(teamID *string) ([]*model.ChannelMember, *model.AppError)
|
||||||
Channels() *Channels
|
Channels() *Channels
|
||||||
@@ -540,6 +541,7 @@ type AppIface interface {
|
|||||||
DisableAutoResponder(c request.CTX, userID string, asAdmin bool) *model.AppError
|
DisableAutoResponder(c request.CTX, userID string, asAdmin bool) *model.AppError
|
||||||
DisableUserAccessToken(token *model.UserAccessToken) *model.AppError
|
DisableUserAccessToken(token *model.UserAccessToken) *model.AppError
|
||||||
DoAppMigrations()
|
DoAppMigrations()
|
||||||
|
DoCheckForAdminNotifications(trial bool) *model.AppError
|
||||||
DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError)
|
DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError)
|
||||||
DoEmojisPermissionsMigration()
|
DoEmojisPermissionsMigration()
|
||||||
DoGuestRolesCreationMigration()
|
DoGuestRolesCreationMigration()
|
||||||
@@ -564,6 +566,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)
|
||||||
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
|
||||||
@@ -907,7 +910,6 @@ type AppIface interface {
|
|||||||
NotificationsLog() *mlog.Logger
|
NotificationsLog() *mlog.Logger
|
||||||
NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError
|
NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError
|
||||||
NotifySharedChannelUserUpdate(user *model.User)
|
NotifySharedChannelUserUpdate(user *model.User)
|
||||||
NotifySystemAdminsToUpgrade(c *request.Context, currentUserTeamID string) *model.AppError
|
|
||||||
OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError
|
OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError
|
||||||
OriginChecker() func(*http.Request) bool
|
OriginChecker() func(*http.Request) bool
|
||||||
PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError)
|
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)
|
SanitizeProfile(user *model.User, asAdmin bool)
|
||||||
SanitizeTeam(session model.Session, team *model.Team) *model.Team
|
SanitizeTeam(session model.Session, team *model.Team) *model.Team
|
||||||
SanitizeTeams(session model.Session, teams []*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)
|
SaveAndBroadcastStatus(status *model.Status)
|
||||||
SaveBrandImage(imageData *multipart.FileHeader) *model.AppError
|
SaveBrandImage(imageData *multipart.FileHeader) *model.AppError
|
||||||
SaveComplianceReport(job *model.Compliance) (*model.Compliance, *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
|
SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError
|
||||||
SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post
|
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)
|
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)
|
SendPasswordReset(email string, siteURL string) (bool, *model.AppError)
|
||||||
SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError
|
SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError
|
||||||
SendTestPushNotification(deviceID string) string
|
SendTestPushNotification(deviceID string) string
|
||||||
@@ -1139,6 +1144,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
|
||||||
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
|
UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError)
|
||||||
VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError
|
VerifyEmailFromToken(c request.CTX, userSuppliedTokenString string) *model.AppError
|
||||||
VerifyUserEmail(userID, email string) *model.AppError
|
VerifyUserEmail(userID, email string) *model.AppError
|
||||||
|
|||||||
93
app/cloud.go
93
app/cloud.go
@@ -4,109 +4,16 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/app/request"
|
|
||||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||||
"github.com/mattermost/mattermost-server/v6/model"
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
"github.com/mattermost/mattermost-server/v6/product"
|
"github.com/mattermost/mattermost-server/v6/product"
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
"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`
|
// Ensure cloud service wrapper implements `product.CloudService`
|
||||||
var _ product.CloudService = (*cloudWrapper)(nil)
|
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
|
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 {
|
func (a *OpenTracingAppLayer) CancelJob(jobId string) *model.AppError {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CancelJob")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CancelJob")
|
||||||
@@ -3653,6 +3670,28 @@ func (a *OpenTracingAppLayer) DoAppMigrations() {
|
|||||||
a.app.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) {
|
func (a *OpenTracingAppLayer) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCommandRequest")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCommandRequest")
|
||||||
@@ -4362,6 +4401,21 @@ func (a *OpenTracingAppLayer) FindTeamByName(name string) bool {
|
|||||||
return resultVar0
|
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) {
|
func (a *OpenTracingAppLayer) GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GenerateMfaSecret")
|
||||||
@@ -12485,28 +12539,6 @@ func (a *OpenTracingAppLayer) NotifySharedChannelUserUpdate(user *model.User) {
|
|||||||
a.app.NotifySharedChannelUserUpdate(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 {
|
func (a *OpenTracingAppLayer) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OpenInteractiveDialog")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.OpenInteractiveDialog")
|
||||||
@@ -14331,6 +14363,50 @@ func (a *OpenTracingAppLayer) SanitizeTeams(session model.Session, teams []*mode
|
|||||||
return resultVar0
|
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) {
|
func (a *OpenTracingAppLayer) SaveAndBroadcastStatus(status *model.Status) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAndBroadcastStatus")
|
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
|
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) {
|
func (a *OpenTracingAppLayer) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPasswordReset")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPasswordReset")
|
||||||
@@ -18003,6 +18101,23 @@ func (a *OpenTracingAppLayer) UpsertGroupSyncable(groupSyncable *model.GroupSync
|
|||||||
return resultVar0, resultVar1
|
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) {
|
func (a *OpenTracingAppLayer) UserCanSeeOtherUser(userID string, otherUserId string) (bool, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UserCanSeeOtherUser")
|
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/import_process"
|
||||||
"github.com/mattermost/mattermost-server/v6/jobs/last_accessible_post"
|
"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/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/product_notices"
|
||||||
"github.com/mattermost/mattermost-server/v6/jobs/resend_invitation_email"
|
"github.com/mattermost/mattermost-server/v6/jobs/resend_invitation_email"
|
||||||
"github.com/mattermost/mattermost-server/v6/model"
|
"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.MakeWorker(s.Jobs, s.License(), New(ServerConnector(s.Channels()))),
|
||||||
last_accessible_post.MakeScheduler(s.Jobs, s.License()),
|
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 {
|
func (s *Server) TelemetryId() string {
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ db/migrations/mysql/000091_create_post_reminder.down.sql
|
|||||||
db/migrations/mysql/000091_create_post_reminder.up.sql
|
db/migrations/mysql/000091_create_post_reminder.up.sql
|
||||||
db/migrations/mysql/000092_add_createat_to_teammembers.down.sql
|
db/migrations/mysql/000092_add_createat_to_teammembers.down.sql
|
||||||
db/migrations/mysql/000092_add_createat_to_teammembers.up.sql
|
db/migrations/mysql/000092_add_createat_to_teammembers.up.sql
|
||||||
|
db/migrations/mysql/000093_notify_admin.down.sql
|
||||||
|
db/migrations/mysql/000093_notify_admin.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
|
||||||
@@ -368,3 +370,5 @@ db/migrations/postgres/000091_create_post_reminder.down.sql
|
|||||||
db/migrations/postgres/000091_create_post_reminder.up.sql
|
db/migrations/postgres/000091_create_post_reminder.up.sql
|
||||||
db/migrations/postgres/000092_add_createat_to_teamembers.down.sql
|
db/migrations/postgres/000092_add_createat_to_teamembers.down.sql
|
||||||
db/migrations/postgres/000092_add_createat_to_teamembers.up.sql
|
db/migrations/postgres/000092_add_createat_to_teamembers.up.sql
|
||||||
|
db/migrations/postgres/000093_notify_admin.down.sql
|
||||||
|
db/migrations/postgres/000093_notify_admin.up.sql
|
||||||
|
|||||||
1
db/migrations/mysql/000093_notify_admin.down.sql
Обычный файл
1
db/migrations/mysql/000093_notify_admin.down.sql
Обычный файл
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS NotifyAdmin;
|
||||||
8
db/migrations/mysql/000093_notify_admin.up.sql
Обычный файл
8
db/migrations/mysql/000093_notify_admin.up.sql
Обычный файл
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS NotifyAdmin (
|
||||||
|
UserId varchar(26) NOT NULL,
|
||||||
|
CreateAt bigint(20) DEFAULT NULL,
|
||||||
|
RequiredPlan varchar(26) NOT NULL,
|
||||||
|
RequiredFeature varchar(100) NOT NULL,
|
||||||
|
Trial BOOLEAN NOT NULL,
|
||||||
|
PRIMARY KEY (UserId, RequiredFeature, RequiredPlan)
|
||||||
|
);
|
||||||
1
db/migrations/postgres/000093_notify_admin.down.sql
Обычный файл
1
db/migrations/postgres/000093_notify_admin.down.sql
Обычный файл
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS NotifyAdmin;
|
||||||
8
db/migrations/postgres/000093_notify_admin.up.sql
Обычный файл
8
db/migrations/postgres/000093_notify_admin.up.sql
Обычный файл
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS NotifyAdmin (
|
||||||
|
UserId varchar(26) NOT NULL,
|
||||||
|
CreateAt bigint DEFAULT NULL,
|
||||||
|
RequiredPlan varchar(26) NOT NULL,
|
||||||
|
RequiredFeature varchar(100) NOT NULL,
|
||||||
|
Trial BOOLEAN NOT NULL,
|
||||||
|
PRIMARY KEY (UserId, RequiredFeature, RequiredPlan)
|
||||||
|
);
|
||||||
28
i18n/en.json
28
i18n/en.json
@@ -495,10 +495,6 @@
|
|||||||
"id": "api.cloud.teams_limit_reached.restore",
|
"id": "api.cloud.teams_limit_reached.restore",
|
||||||
"translation": "Unable to restore team because teams limit has been reached"
|
"translation": "Unable to restore team because teams limit has been reached"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "api.cloud.upgrade_plan_bot_message",
|
|
||||||
"translation": "A member of {{.TeamName}} has notified you to upgrade this workspace."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "api.command.admin_only.app_error",
|
"id": "api.command.admin_only.app_error",
|
||||||
"translation": "Integrations have been limited to admins only."
|
"translation": "Integrations have been limited to admins only."
|
||||||
@@ -4803,6 +4799,22 @@
|
|||||||
"id": "app.cloud.get_subscription_delinquency_date.app_error",
|
"id": "app.cloud.get_subscription_delinquency_date.app_error",
|
||||||
"translation": "Subscription is not delinquent"
|
"translation": "Subscription is not delinquent"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.cloud.trial_plan_bot_message",
|
||||||
|
"translation": "{{.UsersNum}} members of the {{.WorkspaceName}} workspace have requested starting the Enterprise trial for access to: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.cloud.trial_plan_bot_message_single",
|
||||||
|
"translation": "{{.UsersNum}} member of the {{.WorkspaceName}} workspace has requested starting the Enterprise trial for access to: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.cloud.upgrade_plan_bot_message",
|
||||||
|
"translation": "{{.UsersNum}} members of the {{.WorkspaceName}} workspace have requested a workspace upgrade for: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.cloud.upgrade_plan_bot_message_single",
|
||||||
|
"translation": "{{.UsersNum}} member of the {{.WorkspaceName}} workspace has requested a workspace upgrade for: "
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.command.createcommand.internal_error",
|
"id": "app.command.createcommand.internal_error",
|
||||||
"translation": "Unable to save the command."
|
"translation": "Unable to save the command."
|
||||||
@@ -5719,6 +5731,14 @@
|
|||||||
"id": "app.notification.subject.notification.full",
|
"id": "app.notification.subject.notification.full",
|
||||||
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
|
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.notify_admin.save.app_error",
|
||||||
|
"translation": "Unable to save notify data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.notify_admin.send_notification_post.app_error",
|
||||||
|
"translation": "Unable to send notification post."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.oauth.delete_app.app_error",
|
"id": "app.oauth.delete_app.app_error",
|
||||||
"translation": "An error occurred while deleting the OAuth2 App."
|
"translation": "An error occurred while deleting the OAuth2 App."
|
||||||
|
|||||||
25
jobs/notify_admin/scheduler.go
Обычный файл
25
jobs/notify_admin/scheduler.go
Обычный файл
@@ -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 schedFreq = 24 * time.Hour
|
||||||
|
|
||||||
|
func MakeScheduler(jobServer *jobs.JobServer, license *model.License, jobType string) model.Scheduler {
|
||||||
|
isEnabled := func(cfg *model.Config) bool {
|
||||||
|
enabled := license != nil && *license.Features.Cloud
|
||||||
|
mlog.Debug("Scheduler: isEnabled: "+strconv.FormatBool(enabled), mlog.String("scheduler", jobType))
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
return jobs.NewPeriodicScheduler(jobServer, jobType, schedFreq, isEnabled)
|
||||||
|
|
||||||
|
}
|
||||||
50
jobs/notify_admin/worker.go
Обычный файл
50
jobs/notify_admin/worker.go
Обычный файл
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package notify_admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mattermost/mattermost-server/v6/jobs"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UpgradeNotifyJobName = "UpgradeNotifyAdmin"
|
||||||
|
TrialNotifyJobName = "TrialNotifyAdmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppIface interface {
|
||||||
|
DoCheckForAdminNotifications(trial bool) *model.AppError
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeUpgradeNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
|
||||||
|
isEnabled := func(_ *model.Config) bool {
|
||||||
|
return license != nil && license.Features != nil && *license.Features.Cloud
|
||||||
|
}
|
||||||
|
execute := func(_ *model.Job) error {
|
||||||
|
appErr := app.DoCheckForAdminNotifications(false)
|
||||||
|
if appErr != nil {
|
||||||
|
return appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
worker := jobs.NewSimpleWorker(UpgradeNotifyJobName, jobServer, execute, isEnabled)
|
||||||
|
return worker
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeTrialNotifyWorker(jobServer *jobs.JobServer, license *model.License, app AppIface) model.Worker {
|
||||||
|
isEnabled := func(_ *model.Config) bool {
|
||||||
|
return license != nil && license.Features != nil && *license.Features.Cloud
|
||||||
|
}
|
||||||
|
execute := func(_ *model.Job) error {
|
||||||
|
appErr := app.DoCheckForAdminNotifications(true)
|
||||||
|
if appErr != nil {
|
||||||
|
return appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
worker := jobs.NewSimpleWorker(TrialNotifyJobName, jobServer, execute, isEnabled)
|
||||||
|
return worker
|
||||||
|
}
|
||||||
@@ -8020,20 +8020,36 @@ func (c *Client4) ValidateWorkspaceBusinessEmail() (*Response, error) {
|
|||||||
return BuildResponse(r), nil
|
return BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client4) NotifyAdmin(nr *NotifyAdminToUpgradeRequest) int {
|
func (c *Client4) NotifyAdmin(nr *NotifyAdminToUpgradeRequest) (int, error) {
|
||||||
nrJSON, err := json.Marshal(nr)
|
nrJSON, err := json.Marshal(nr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := c.DoAPIPost(c.cloudRoute()+"/notify-admin-to-upgrade", string(nrJSON))
|
r, err := c.DoAPIPost("/users/notify-admin", string(nrJSON))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r.StatusCode
|
return r.StatusCode, err
|
||||||
}
|
}
|
||||||
|
|
||||||
closeBody(r)
|
closeBody(r)
|
||||||
|
|
||||||
return r.StatusCode
|
return r.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client4) TriggerNotifyAdmin(nr *NotifyAdminToUpgradeRequest) (int, error) {
|
||||||
|
nrJSON, err := json.Marshal(nr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.DoAPIPost("/users/trigger-notify-admin-posts", string(nrJSON))
|
||||||
|
if err != nil {
|
||||||
|
return r.StatusCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBody(r)
|
||||||
|
|
||||||
|
return r.StatusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client4) ValidateBusinessEmail(email *ValidateBusinessEmailRequest) (*Response, error) {
|
func (c *Client4) ValidateBusinessEmail(email *ValidateBusinessEmailRequest) (*Response, error) {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,9 +40,6 @@ const (
|
|||||||
SubscriptionFamilyOnPrem = SubscriptionFamily("on-prem")
|
SubscriptionFamilyOnPrem = SubscriptionFamily("on-prem")
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultCloudNotifyAdminCoolOffDays = 30
|
|
||||||
const CloudNotifyAdminInfo = "cloud_notify_admin_info"
|
|
||||||
|
|
||||||
// Product model represents a product on the cloud system.
|
// Product model represents a product on the cloud system.
|
||||||
type Product struct {
|
type Product struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -258,23 +253,3 @@ type ProductLimits struct {
|
|||||||
Messages *MessagesLimits `json:"messages,omitempty"`
|
Messages *MessagesLimits `json:"messages,omitempty"`
|
||||||
Teams *TeamsLimits `json:"teams,omitempty"`
|
Teams *TeamsLimits `json:"teams,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotifyAdminToUpgradeRequest struct {
|
|
||||||
CurrentTeamId string `json:"current_team_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminNotificationUserInfo struct {
|
|
||||||
LastUserIDToNotify string
|
|
||||||
LastNotificationTimestamp int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func CanNotify(lastNotificationTimestamp int64) bool {
|
|
||||||
coolOffPeriodDaysEnv := os.Getenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS")
|
|
||||||
coolOffPeriodDays, parseError := strconv.ParseFloat(coolOffPeriodDaysEnv, 64)
|
|
||||||
if parseError != nil {
|
|
||||||
coolOffPeriodDays = defaultCloudNotifyAdminCoolOffDays
|
|
||||||
}
|
|
||||||
daysToMillis := coolOffPeriodDays * 24 * 60 * 60 * 1000
|
|
||||||
timeDiff := GetMillis() - lastNotificationTimestamp
|
|
||||||
return timeDiff >= int64(daysToMillis)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ type ServiceSettings struct {
|
|||||||
ExperimentalEnableDefaultChannelLeaveJoinMessages *bool `access:"experimental_features"`
|
ExperimentalEnableDefaultChannelLeaveJoinMessages *bool `access:"experimental_features"`
|
||||||
ExperimentalGroupUnreadChannels *string `access:"experimental_features"`
|
ExperimentalGroupUnreadChannels *string `access:"experimental_features"`
|
||||||
EnableAPITeamDeletion *bool
|
EnableAPITeamDeletion *bool
|
||||||
|
EnableAPITriggerAdminNotifications *bool
|
||||||
EnableAPIUserDeletion *bool
|
EnableAPIUserDeletion *bool
|
||||||
ExperimentalEnableHardenedMode *bool `access:"experimental_features"`
|
ExperimentalEnableHardenedMode *bool `access:"experimental_features"`
|
||||||
ExperimentalStrictCSRFEnforcement *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
ExperimentalStrictCSRFEnforcement *bool `access:"experimental_features,write_restrictable,cloud_restrictable"`
|
||||||
@@ -754,6 +755,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
|
|||||||
s.EnableAPITeamDeletion = NewBool(false)
|
s.EnableAPITeamDeletion = NewBool(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.EnableAPITriggerAdminNotifications == nil {
|
||||||
|
s.EnableAPITriggerAdminNotifications = NewBool(false)
|
||||||
|
}
|
||||||
|
|
||||||
if s.EnableAPIUserDeletion == nil {
|
if s.EnableAPIUserDeletion == nil {
|
||||||
s.EnableAPIUserDeletion = NewBool(false)
|
s.EnableAPIUserDeletion = NewBool(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const (
|
|||||||
JobTypeResendInvitationEmail = "resend_invitation_email"
|
JobTypeResendInvitationEmail = "resend_invitation_email"
|
||||||
JobTypeExtractContent = "extract_content"
|
JobTypeExtractContent = "extract_content"
|
||||||
JobTypeLastAccessiblePost = "last_accessible_post"
|
JobTypeLastAccessiblePost = "last_accessible_post"
|
||||||
|
JobTypeUpgradeNotifyAdmin = "upgrade_notify_admin"
|
||||||
|
JobTypeTrialNotifyAdmin = "trial_notify_admin"
|
||||||
|
|
||||||
JobStatusPending = "pending"
|
JobStatusPending = "pending"
|
||||||
JobStatusInProgress = "in_progress"
|
JobStatusInProgress = "in_progress"
|
||||||
|
|||||||
75
model/notify_admin.go
Обычный файл
75
model/notify_admin.go
Обычный файл
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MattermostPaidFeature string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaidFeatureGuestAccounts = MattermostPaidFeature("mattermost.feature.guest_accounts")
|
||||||
|
PaidFeatureCustomUsergroups = MattermostPaidFeature("mattermost.feature.custom_user_groups")
|
||||||
|
PaidFeatureCreateMultipleTeams = MattermostPaidFeature("mattermost.feature.create_multiple_teams")
|
||||||
|
PaidFeatureStartcall = MattermostPaidFeature("mattermost.feature.start_call")
|
||||||
|
PaidFeaturePlaybooksRetrospective = MattermostPaidFeature("mattermost.feature.playbooks_retro")
|
||||||
|
PaidFeatureUnlimitedMessages = MattermostPaidFeature("mattermost.feature.unlimited_messages")
|
||||||
|
PaidFeatureUnlimitedFileStorage = MattermostPaidFeature("mattermost.feature.unlimited_file_storage")
|
||||||
|
PaidFeatureUnlimitedIntegrations = MattermostPaidFeature("mattermost.feature.unlimited_integrations")
|
||||||
|
PaidFeatureUnlimitedBoardcards = MattermostPaidFeature("mattermost.feature.unlimited_board_cards")
|
||||||
|
PaidFeatureAllProfessionalfeatures = MattermostPaidFeature("mattermost.feature.all_professional")
|
||||||
|
PaidFeatureAllEnterprisefeatures = MattermostPaidFeature("mattermost.feature.all_enterprise")
|
||||||
|
)
|
||||||
|
|
||||||
|
var validSKUs map[string]struct{} = map[string]struct{}{
|
||||||
|
LicenseShortSkuProfessional: {},
|
||||||
|
LicenseShortSkuEnterprise: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are the features a non admin would typically ping an admin about
|
||||||
|
var paidFeatures map[MattermostPaidFeature]struct{} = map[MattermostPaidFeature]struct{}{
|
||||||
|
PaidFeatureGuestAccounts: {},
|
||||||
|
PaidFeatureCustomUsergroups: {},
|
||||||
|
PaidFeatureCreateMultipleTeams: {},
|
||||||
|
PaidFeatureStartcall: {},
|
||||||
|
PaidFeaturePlaybooksRetrospective: {},
|
||||||
|
PaidFeatureUnlimitedMessages: {},
|
||||||
|
PaidFeatureUnlimitedFileStorage: {},
|
||||||
|
PaidFeatureUnlimitedIntegrations: {},
|
||||||
|
PaidFeatureUnlimitedBoardcards: {},
|
||||||
|
PaidFeatureAllProfessionalfeatures: {},
|
||||||
|
PaidFeatureAllEnterprisefeatures: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotifyAdminToUpgradeRequest struct {
|
||||||
|
TrialNotification bool `json:"trial_notification"`
|
||||||
|
RequiredPlan string `json:"required_plan"`
|
||||||
|
RequiredFeature MattermostPaidFeature `json:"required_feature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotifyAdminData struct {
|
||||||
|
CreateAt int64 `json:"create_at,omitempty"`
|
||||||
|
UserId string `json:"user_id"`
|
||||||
|
RequiredPlan string `json:"required_plan"`
|
||||||
|
RequiredFeature MattermostPaidFeature `json:"required_feature"`
|
||||||
|
Trial bool `json:"trial"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nad *NotifyAdminData) IsValid() *AppError {
|
||||||
|
if _, planOk := validSKUs[nad.RequiredPlan]; !planOk {
|
||||||
|
return NewAppError("NotifyAdmin.IsValid", fmt.Sprintf("Invalid plan, %s provided", nad.RequiredPlan), nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, featureOk := paidFeatures[nad.RequiredFeature]; !featureOk {
|
||||||
|
return NewAppError("NotifyAdmin.IsValid", fmt.Sprintf("Invalid feature, %s provided", nad.RequiredFeature), nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nad *NotifyAdminData) PreSave() {
|
||||||
|
nad.CreateAt = GetMillis()
|
||||||
|
}
|
||||||
@@ -428,6 +428,7 @@ func (ts *TelemetryService) trackConfig() {
|
|||||||
"websocket_url": isDefault(*cfg.ServiceSettings.WebsocketURL, ""),
|
"websocket_url": isDefault(*cfg.ServiceSettings.WebsocketURL, ""),
|
||||||
"allow_cookies_for_subdomains": *cfg.ServiceSettings.AllowCookiesForSubdomains,
|
"allow_cookies_for_subdomains": *cfg.ServiceSettings.AllowCookiesForSubdomains,
|
||||||
"enable_api_team_deletion": *cfg.ServiceSettings.EnableAPITeamDeletion,
|
"enable_api_team_deletion": *cfg.ServiceSettings.EnableAPITeamDeletion,
|
||||||
|
"enable_api_trigger_admin_notification": *cfg.ServiceSettings.EnableAPITriggerAdminNotifications,
|
||||||
"enable_api_user_deletion": *cfg.ServiceSettings.EnableAPIUserDeletion,
|
"enable_api_user_deletion": *cfg.ServiceSettings.EnableAPIUserDeletion,
|
||||||
"enable_api_channel_deletion": *cfg.ServiceSettings.EnableAPIChannelDeletion,
|
"enable_api_channel_deletion": *cfg.ServiceSettings.EnableAPIChannelDeletion,
|
||||||
"experimental_enable_hardened_mode": *cfg.ServiceSettings.ExperimentalEnableHardenedMode,
|
"experimental_enable_hardened_mode": *cfg.ServiceSettings.ExperimentalEnableHardenedMode,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type OpenTracingLayer struct {
|
|||||||
JobStore store.JobStore
|
JobStore store.JobStore
|
||||||
LicenseStore store.LicenseStore
|
LicenseStore store.LicenseStore
|
||||||
LinkMetadataStore store.LinkMetadataStore
|
LinkMetadataStore store.LinkMetadataStore
|
||||||
|
NotifyAdminStore store.NotifyAdminStore
|
||||||
OAuthStore store.OAuthStore
|
OAuthStore store.OAuthStore
|
||||||
PluginStore store.PluginStore
|
PluginStore store.PluginStore
|
||||||
PostStore store.PostStore
|
PostStore store.PostStore
|
||||||
@@ -114,6 +115,10 @@ func (s *OpenTracingLayer) LinkMetadata() store.LinkMetadataStore {
|
|||||||
return s.LinkMetadataStore
|
return s.LinkMetadataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayer) NotifyAdmin() store.NotifyAdminStore {
|
||||||
|
return s.NotifyAdminStore
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OpenTracingLayer) OAuth() store.OAuthStore {
|
func (s *OpenTracingLayer) OAuth() store.OAuthStore {
|
||||||
return s.OAuthStore
|
return s.OAuthStore
|
||||||
}
|
}
|
||||||
@@ -276,6 +281,11 @@ type OpenTracingLayerLinkMetadataStore struct {
|
|||||||
Root *OpenTracingLayer
|
Root *OpenTracingLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenTracingLayerNotifyAdminStore struct {
|
||||||
|
store.NotifyAdminStore
|
||||||
|
Root *OpenTracingLayer
|
||||||
|
}
|
||||||
|
|
||||||
type OpenTracingLayerOAuthStore struct {
|
type OpenTracingLayerOAuthStore struct {
|
||||||
store.OAuthStore
|
store.OAuthStore
|
||||||
Root *OpenTracingLayer
|
Root *OpenTracingLayer
|
||||||
@@ -4970,6 +4980,78 @@ func (s *OpenTracingLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadat
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.DeleteBefore")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
err := s.NotifyAdminStore.DeleteBefore(trial, now)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Get")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
result, err := s.NotifyAdminStore.Get(trial)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.GetDataByUserIdAndFeature")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "NotifyAdminStore.Save")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
result, err := s.NotifyAdminStore.Save(data)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, 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")
|
||||||
@@ -12333,6 +12415,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
|
|||||||
newStore.JobStore = &OpenTracingLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
|
newStore.JobStore = &OpenTracingLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
|
||||||
newStore.LicenseStore = &OpenTracingLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
|
newStore.LicenseStore = &OpenTracingLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
|
||||||
newStore.LinkMetadataStore = &OpenTracingLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
|
newStore.LinkMetadataStore = &OpenTracingLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
|
||||||
|
newStore.NotifyAdminStore = &OpenTracingLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
|
||||||
newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
|
newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
|
||||||
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
||||||
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type RetryLayer struct {
|
|||||||
JobStore store.JobStore
|
JobStore store.JobStore
|
||||||
LicenseStore store.LicenseStore
|
LicenseStore store.LicenseStore
|
||||||
LinkMetadataStore store.LinkMetadataStore
|
LinkMetadataStore store.LinkMetadataStore
|
||||||
|
NotifyAdminStore store.NotifyAdminStore
|
||||||
OAuthStore store.OAuthStore
|
OAuthStore store.OAuthStore
|
||||||
PluginStore store.PluginStore
|
PluginStore store.PluginStore
|
||||||
PostStore store.PostStore
|
PostStore store.PostStore
|
||||||
@@ -117,6 +118,10 @@ func (s *RetryLayer) LinkMetadata() store.LinkMetadataStore {
|
|||||||
return s.LinkMetadataStore
|
return s.LinkMetadataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayer) NotifyAdmin() store.NotifyAdminStore {
|
||||||
|
return s.NotifyAdminStore
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayer) OAuth() store.OAuthStore {
|
func (s *RetryLayer) OAuth() store.OAuthStore {
|
||||||
return s.OAuthStore
|
return s.OAuthStore
|
||||||
}
|
}
|
||||||
@@ -279,6 +284,11 @@ type RetryLayerLinkMetadataStore struct {
|
|||||||
Root *RetryLayer
|
Root *RetryLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RetryLayerNotifyAdminStore struct {
|
||||||
|
store.NotifyAdminStore
|
||||||
|
Root *RetryLayer
|
||||||
|
}
|
||||||
|
|
||||||
type RetryLayerOAuthStore struct {
|
type RetryLayerOAuthStore struct {
|
||||||
store.OAuthStore
|
store.OAuthStore
|
||||||
Root *RetryLayer
|
Root *RetryLayer
|
||||||
@@ -5633,6 +5643,90 @@ func (s *RetryLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*m
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
err := s.NotifyAdminStore.DeleteBefore(trial, 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 *RetryLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.NotifyAdminStore.Get(trial)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.NotifyAdminStore.Save(data)
|
||||||
|
if err == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayerOAuthStore) DeleteApp(id string) error {
|
func (s *RetryLayerOAuthStore) DeleteApp(id string) error {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
@@ -14058,6 +14152,7 @@ func New(childStore store.Store) *RetryLayer {
|
|||||||
newStore.JobStore = &RetryLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
|
newStore.JobStore = &RetryLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
|
||||||
newStore.LicenseStore = &RetryLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
|
newStore.LicenseStore = &RetryLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
|
||||||
newStore.LinkMetadataStore = &RetryLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
|
newStore.LinkMetadataStore = &RetryLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
|
||||||
|
newStore.NotifyAdminStore = &RetryLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
|
||||||
newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
|
newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
|
||||||
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
||||||
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ func genStore() *mocks.Store {
|
|||||||
mock.On("UserAccessToken").Return(&mocks.UserAccessTokenStore{})
|
mock.On("UserAccessToken").Return(&mocks.UserAccessTokenStore{})
|
||||||
mock.On("UserTermsOfService").Return(&mocks.UserTermsOfServiceStore{})
|
mock.On("UserTermsOfService").Return(&mocks.UserTermsOfServiceStore{})
|
||||||
mock.On("Webhook").Return(&mocks.WebhookStore{})
|
mock.On("Webhook").Return(&mocks.WebhookStore{})
|
||||||
|
mock.On("NotifyAdmin").Return(&mocks.NotifyAdminStore{})
|
||||||
return mock
|
return mock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
87
store/sqlstore/notify_admin_store.go
Обычный файл
87
store/sqlstore/notify_admin_store.go
Обычный файл
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package sqlstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/store"
|
||||||
|
sq "github.com/mattermost/squirrel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqlNotifyAdminStore struct {
|
||||||
|
*SqlStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSqlNotifyAdminStore(sqlStore *SqlStore) store.NotifyAdminStore {
|
||||||
|
return &SqlNotifyAdminStore{sqlStore}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlNotifyAdminStore) insert(data *model.NotifyAdminData) (sql.Result, error) {
|
||||||
|
query := `INSERT INTO NotifyAdmin (UserId, CreateAt, RequiredPlan, RequiredFeature, Trial) VALUES (:UserId, :CreateAt, :RequiredPlan, :RequiredFeature, :Trial)`
|
||||||
|
return s.GetMasterX().NamedExec(query, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
|
||||||
|
if err := data.IsValid(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data.PreSave()
|
||||||
|
|
||||||
|
_, err := s.insert(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to save Notify Admin data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) {
|
||||||
|
data := []*model.NotifyAdminData{}
|
||||||
|
query, args, err := s.getQueryBuilder().
|
||||||
|
Select("*").
|
||||||
|
From("NotifyAdmin").
|
||||||
|
Where(sq.Eq{"UserId": userId, "RequiredFeature": feature}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not build sql query to get all notifcation data by user id and required feature")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, store.NewErrNotFound("NotifyAdmin", fmt.Sprintf("user id: %s and required feature: %s", userId, feature))
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(err, "notifcation data by user id: %s and required feature: %s", userId, feature)
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SqlNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
|
||||||
|
data := []*model.NotifyAdminData{}
|
||||||
|
query, args, err := s.getQueryBuilder().
|
||||||
|
Select("*").
|
||||||
|
From("NotifyAdmin").
|
||||||
|
Where(sq.Eq{"trial": trial}).
|
||||||
|
ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not build sql query to get all notifcation data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.GetReplicaX().Select(&data, query, args...); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "notifcation data")
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return errors.Wrapf(err, "failed to remove all notification data with trial=%t", trial)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
14
store/sqlstore/notify_admin_store_test.go
Обычный файл
14
store/sqlstore/notify_admin_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package sqlstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v6/store/storetest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotifyAdminStore(t *testing.T) {
|
||||||
|
StoreTest(t, storetest.TestNotifyAdminStore)
|
||||||
|
}
|
||||||
@@ -108,6 +108,7 @@ type SqlStoreStores struct {
|
|||||||
UserTermsOfService store.UserTermsOfServiceStore
|
UserTermsOfService store.UserTermsOfServiceStore
|
||||||
linkMetadata store.LinkMetadataStore
|
linkMetadata store.LinkMetadataStore
|
||||||
sharedchannel store.SharedChannelStore
|
sharedchannel store.SharedChannelStore
|
||||||
|
notifyAdmin store.NotifyAdminStore
|
||||||
}
|
}
|
||||||
|
|
||||||
type SqlStore struct {
|
type SqlStore struct {
|
||||||
@@ -212,6 +213,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
|
|||||||
store.stores.scheme = newSqlSchemeStore(store)
|
store.stores.scheme = newSqlSchemeStore(store)
|
||||||
store.stores.group = newSqlGroupStore(store)
|
store.stores.group = newSqlGroupStore(store)
|
||||||
store.stores.productNotices = newSqlProductNoticesStore(store)
|
store.stores.productNotices = newSqlProductNoticesStore(store)
|
||||||
|
store.stores.notifyAdmin = newSqlNotifyAdminStore(store)
|
||||||
|
|
||||||
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
|
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
|
||||||
|
|
||||||
@@ -914,6 +916,10 @@ func (ss *SqlStore) LinkMetadata() store.LinkMetadataStore {
|
|||||||
return ss.stores.linkMetadata
|
return ss.stores.linkMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ss *SqlStore) NotifyAdmin() store.NotifyAdminStore {
|
||||||
|
return ss.stores.notifyAdmin
|
||||||
|
}
|
||||||
|
|
||||||
func (ss *SqlStore) SharedChannel() store.SharedChannelStore {
|
func (ss *SqlStore) SharedChannel() store.SharedChannelStore {
|
||||||
return ss.stores.sharedchannel
|
return ss.stores.sharedchannel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ type Store interface {
|
|||||||
CheckIntegrity() <-chan model.IntegrityCheckResult
|
CheckIntegrity() <-chan model.IntegrityCheckResult
|
||||||
SetContext(context context.Context)
|
SetContext(context context.Context)
|
||||||
Context() context.Context
|
Context() context.Context
|
||||||
|
NotifyAdmin() NotifyAdminStore
|
||||||
}
|
}
|
||||||
|
|
||||||
type RetentionPolicyStore interface {
|
type RetentionPolicyStore interface {
|
||||||
@@ -923,6 +924,13 @@ type LinkMetadataStore interface {
|
|||||||
Get(url string, timestamp int64) (*model.LinkMetadata, error)
|
Get(url string, timestamp int64) (*model.LinkMetadata, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotifyAdminStore interface {
|
||||||
|
Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error)
|
||||||
|
GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error)
|
||||||
|
Get(trial bool) ([]*model.NotifyAdminData, error)
|
||||||
|
DeleteBefore(trial bool, now int64) error
|
||||||
|
}
|
||||||
|
|
||||||
type SharedChannelStore interface {
|
type SharedChannelStore interface {
|
||||||
Save(sc *model.SharedChannel) (*model.SharedChannel, error)
|
Save(sc *model.SharedChannel) (*model.SharedChannel, error)
|
||||||
Get(channelId string) (*model.SharedChannel, error)
|
Get(channelId string) (*model.SharedChannel, error)
|
||||||
|
|||||||
98
store/storetest/mocks/NotifyAdminStore.go
Обычный файл
98
store/storetest/mocks/NotifyAdminStore.go
Обычный файл
@@ -0,0 +1,98 @@
|
|||||||
|
// Code generated by mockery v2.10.4. DO NOT EDIT.
|
||||||
|
|
||||||
|
// Regenerate this file using `make store-mocks`.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
model "github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifyAdminStore is an autogenerated mock type for the NotifyAdminStore type
|
||||||
|
type NotifyAdminStore struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBefore provides a mock function with given fields: trial, now
|
||||||
|
func (_m *NotifyAdminStore) DeleteBefore(trial bool, now int64) error {
|
||||||
|
ret := _m.Called(trial, now)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(bool, int64) error); ok {
|
||||||
|
r0 = rf(trial, now)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provides a mock function with given fields: trial
|
||||||
|
func (_m *NotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
|
||||||
|
ret := _m.Called(trial)
|
||||||
|
|
||||||
|
var r0 []*model.NotifyAdminData
|
||||||
|
if rf, ok := ret.Get(0).(func(bool) []*model.NotifyAdminData); ok {
|
||||||
|
r0 = rf(trial)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.NotifyAdminData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(bool) error); ok {
|
||||||
|
r1 = rf(trial)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDataByUserIdAndFeature provides a mock function with given fields: userId, feature
|
||||||
|
func (_m *NotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) {
|
||||||
|
ret := _m.Called(userId, feature)
|
||||||
|
|
||||||
|
var r0 []*model.NotifyAdminData
|
||||||
|
if rf, ok := ret.Get(0).(func(string, model.MattermostPaidFeature) []*model.NotifyAdminData); ok {
|
||||||
|
r0 = rf(userId, feature)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.NotifyAdminData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(string, model.MattermostPaidFeature) error); ok {
|
||||||
|
r1 = rf(userId, feature)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save provides a mock function with given fields: data
|
||||||
|
func (_m *NotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
|
||||||
|
ret := _m.Called(data)
|
||||||
|
|
||||||
|
var r0 *model.NotifyAdminData
|
||||||
|
if rf, ok := ret.Get(0).(func(*model.NotifyAdminData) *model.NotifyAdminData); ok {
|
||||||
|
r0 = rf(data)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.NotifyAdminData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(*model.NotifyAdminData) error); ok {
|
||||||
|
r1 = rf(data)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
@@ -395,6 +395,22 @@ func (_m *Store) MarkSystemRanUnitTests() {
|
|||||||
_m.Called()
|
_m.Called()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotifyAdmin provides a mock function with given fields:
|
||||||
|
func (_m *Store) NotifyAdmin() store.NotifyAdminStore {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 store.NotifyAdminStore
|
||||||
|
if rf, ok := ret.Get(0).(func() store.NotifyAdminStore); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(store.NotifyAdminStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// OAuth provides a mock function with given fields:
|
// OAuth provides a mock function with given fields:
|
||||||
func (_m *Store) OAuth() store.OAuthStore {
|
func (_m *Store) OAuth() store.OAuthStore {
|
||||||
ret := _m.Called()
|
ret := _m.Called()
|
||||||
|
|||||||
194
store/storetest/notify_admin_store.go
Обычный файл
194
store/storetest/notify_admin_store.go
Обычный файл
@@ -0,0 +1,194 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package storetest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/store"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotifyAdminStore(t *testing.T, ss store.Store) {
|
||||||
|
t.Run("Save", func(t *testing.T) { testNotifyAdminStoreSave(t, ss) })
|
||||||
|
t.Run("testGetDataByUserIdAndFeature", func(t *testing.T) { testGetDataByUserIdAndFeature(t, ss) })
|
||||||
|
t.Run("testGet", func(t *testing.T) { testGet(t, ss) })
|
||||||
|
t.Run("testDeleteBefore", func(t *testing.T) { testDeleteBefore(t, ss) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func tearDown(t *testing.T, ss store.Store) {
|
||||||
|
err := ss.NotifyAdmin().DeleteBefore(true, model.GetMillis()+model.GetMillis())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ss.NotifyAdmin().DeleteBefore(false, model.GetMillis()+model.GetMillis())
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotifyAdminStoreSave(t *testing.T, ss store.Store) {
|
||||||
|
d1 := &model.NotifyAdminData{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ss.NotifyAdmin().Save(d1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// unknow plan error
|
||||||
|
d2 := &model.NotifyAdminData{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: "Unknown feature",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ss.NotifyAdmin().Save(d2)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// unknown feature error
|
||||||
|
d3 := &model.NotifyAdminData{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
RequiredPlan: "Unknown plan",
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
}
|
||||||
|
_, err = ss.NotifyAdmin().Save(d3)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// same user requesting same feature error
|
||||||
|
singleUserId := model.NewId()
|
||||||
|
d5 := &model.NotifyAdminData{
|
||||||
|
UserId: singleUserId,
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
}
|
||||||
|
_, err = ss.NotifyAdmin().Save(d5)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
d6 := &model.NotifyAdminData{
|
||||||
|
UserId: singleUserId,
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
}
|
||||||
|
_, err = ss.NotifyAdmin().Save(d6)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
tearDown(t, ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGet(t *testing.T, ss store.Store) {
|
||||||
|
userId1 := model.NewId()
|
||||||
|
d1 := &model.NotifyAdminData{
|
||||||
|
UserId: userId1,
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ss.NotifyAdmin().Save(d1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
d1Trial := &model.NotifyAdminData{
|
||||||
|
UserId: userId1,
|
||||||
|
RequiredPlan: model.LicenseShortSkuEnterprise,
|
||||||
|
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
|
||||||
|
Trial: true,
|
||||||
|
}
|
||||||
|
_, err = ss.NotifyAdmin().Save(d1Trial)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
d1Trial2 := &model.NotifyAdminData{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
RequiredPlan: model.LicenseShortSkuEnterprise,
|
||||||
|
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
|
||||||
|
Trial: true,
|
||||||
|
}
|
||||||
|
_, err = ss.NotifyAdmin().Save(d1Trial2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
upgradeRequests, err := ss.NotifyAdmin().Get(false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, len(upgradeRequests), 1)
|
||||||
|
|
||||||
|
trialRequests, err := ss.NotifyAdmin().Get(true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, len(trialRequests), 2)
|
||||||
|
|
||||||
|
tearDown(t, ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetDataByUserIdAndFeature(t *testing.T, ss store.Store) {
|
||||||
|
userId1 := model.NewId()
|
||||||
|
d1 := &model.NotifyAdminData{
|
||||||
|
UserId: userId1,
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ss.NotifyAdmin().Save(d1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userId2 := model.NewId()
|
||||||
|
d2 := &model.NotifyAdminData{
|
||||||
|
UserId: userId2,
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureCustomUsergroups,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ss.NotifyAdmin().Save(d2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user1Request, err := ss.NotifyAdmin().GetDataByUserIdAndFeature(userId1, model.PaidFeatureAllProfessionalfeatures)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, len(user1Request), 1)
|
||||||
|
require.Equal(t, user1Request[0].RequiredFeature, model.PaidFeatureAllProfessionalfeatures)
|
||||||
|
|
||||||
|
tearDown(t, ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteBefore(t *testing.T, ss store.Store) {
|
||||||
|
userId1 := model.NewId()
|
||||||
|
d1 := &model.NotifyAdminData{
|
||||||
|
UserId: userId1,
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ss.NotifyAdmin().Save(d1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
d1Trial := &model.NotifyAdminData{
|
||||||
|
UserId: userId1,
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
|
||||||
|
Trial: true,
|
||||||
|
}
|
||||||
|
_, err = ss.NotifyAdmin().Save(d1Trial)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
d1Trial2 := &model.NotifyAdminData{
|
||||||
|
UserId: model.NewId(),
|
||||||
|
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||||
|
RequiredFeature: model.PaidFeatureAllEnterprisefeatures,
|
||||||
|
Trial: true,
|
||||||
|
}
|
||||||
|
_, err = ss.NotifyAdmin().Save(d1Trial2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ss.NotifyAdmin().DeleteBefore(false, model.GetMillis()+model.GetMillis()) // delete all upgrade requests
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
upgradeRequests, err := ss.NotifyAdmin().Get(false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, len(upgradeRequests), 0)
|
||||||
|
|
||||||
|
trialRequests, err := ss.NotifyAdmin().Get(true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, len(trialRequests), 2) // trial requests should still exist
|
||||||
|
|
||||||
|
err = ss.NotifyAdmin().DeleteBefore(true, model.GetMillis()+model.GetMillis()) // delete all trial requests
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
trialRequests, err = ss.NotifyAdmin().Get(false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, len(trialRequests), 0)
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ type Store struct {
|
|||||||
SharedChannelStore mocks.SharedChannelStore
|
SharedChannelStore mocks.SharedChannelStore
|
||||||
ProductNoticesStore mocks.ProductNoticesStore
|
ProductNoticesStore mocks.ProductNoticesStore
|
||||||
context context.Context
|
context context.Context
|
||||||
|
NotifyAdminStore mocks.NotifyAdminStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) SetContext(context context.Context) { s.context = context }
|
func (s *Store) SetContext(context context.Context) { s.context = context }
|
||||||
@@ -95,6 +96,7 @@ func (s *Store) UserTermsOfService() store.UserTermsOfServiceStore { return &s.U
|
|||||||
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
|
func (s *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore {
|
||||||
return &s.ChannelMemberHistoryStore
|
return &s.ChannelMemberHistoryStore
|
||||||
}
|
}
|
||||||
|
func (s *Store) NotifyAdmin() store.NotifyAdminStore { return &s.NotifyAdminStore }
|
||||||
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
|
func (s *Store) Group() store.GroupStore { return &s.GroupStore }
|
||||||
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
|
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
|
||||||
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
|
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
|
||||||
@@ -154,5 +156,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
|
|||||||
&s.ThreadStore,
|
&s.ThreadStore,
|
||||||
&s.ProductNoticesStore,
|
&s.ProductNoticesStore,
|
||||||
&s.SharedChannelStore,
|
&s.SharedChannelStore,
|
||||||
|
&s.NotifyAdminStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type TimerLayer struct {
|
|||||||
JobStore store.JobStore
|
JobStore store.JobStore
|
||||||
LicenseStore store.LicenseStore
|
LicenseStore store.LicenseStore
|
||||||
LinkMetadataStore store.LinkMetadataStore
|
LinkMetadataStore store.LinkMetadataStore
|
||||||
|
NotifyAdminStore store.NotifyAdminStore
|
||||||
OAuthStore store.OAuthStore
|
OAuthStore store.OAuthStore
|
||||||
PluginStore store.PluginStore
|
PluginStore store.PluginStore
|
||||||
PostStore store.PostStore
|
PostStore store.PostStore
|
||||||
@@ -113,6 +114,10 @@ func (s *TimerLayer) LinkMetadata() store.LinkMetadataStore {
|
|||||||
return s.LinkMetadataStore
|
return s.LinkMetadataStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayer) NotifyAdmin() store.NotifyAdminStore {
|
||||||
|
return s.NotifyAdminStore
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayer) OAuth() store.OAuthStore {
|
func (s *TimerLayer) OAuth() store.OAuthStore {
|
||||||
return s.OAuthStore
|
return s.OAuthStore
|
||||||
}
|
}
|
||||||
@@ -275,6 +280,11 @@ type TimerLayerLinkMetadataStore struct {
|
|||||||
Root *TimerLayer
|
Root *TimerLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimerLayerNotifyAdminStore struct {
|
||||||
|
store.NotifyAdminStore
|
||||||
|
Root *TimerLayer
|
||||||
|
}
|
||||||
|
|
||||||
type TimerLayerOAuthStore struct {
|
type TimerLayerOAuthStore struct {
|
||||||
store.OAuthStore
|
store.OAuthStore
|
||||||
Root *TimerLayer
|
Root *TimerLayer
|
||||||
@@ -4507,6 +4517,70 @@ func (s *TimerLayerLinkMetadataStore) Save(linkMetadata *model.LinkMetadata) (*m
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerNotifyAdminStore) DeleteBefore(trial bool, now int64) error {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
err := s.NotifyAdminStore.DeleteBefore(trial, 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.DeleteBefore", success, elapsed)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerNotifyAdminStore) Get(trial bool) ([]*model.NotifyAdminData, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.NotifyAdminStore.Get(trial)
|
||||||
|
|
||||||
|
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.Get", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerNotifyAdminStore) GetDataByUserIdAndFeature(userId string, feature model.MattermostPaidFeature) ([]*model.NotifyAdminData, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.NotifyAdminStore.GetDataByUserIdAndFeature(userId, feature)
|
||||||
|
|
||||||
|
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.GetDataByUserIdAndFeature", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerNotifyAdminStore) Save(data *model.NotifyAdminData) (*model.NotifyAdminData, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.NotifyAdminStore.Save(data)
|
||||||
|
|
||||||
|
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.Save", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerOAuthStore) DeleteApp(id string) error {
|
func (s *TimerLayerOAuthStore) DeleteApp(id string) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
@@ -11112,6 +11186,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
|
|||||||
newStore.JobStore = &TimerLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
|
newStore.JobStore = &TimerLayerJobStore{JobStore: childStore.Job(), Root: &newStore}
|
||||||
newStore.LicenseStore = &TimerLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
|
newStore.LicenseStore = &TimerLayerLicenseStore{LicenseStore: childStore.License(), Root: &newStore}
|
||||||
newStore.LinkMetadataStore = &TimerLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
|
newStore.LinkMetadataStore = &TimerLayerLinkMetadataStore{LinkMetadataStore: childStore.LinkMetadata(), Root: &newStore}
|
||||||
|
newStore.NotifyAdminStore = &TimerLayerNotifyAdminStore{NotifyAdminStore: childStore.NotifyAdmin(), Root: &newStore}
|
||||||
newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
|
newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
|
||||||
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
||||||
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
|
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
|
||||||
"ExperimentalGroupUnreadChannels": "disabled",
|
"ExperimentalGroupUnreadChannels": "disabled",
|
||||||
"EnableAPITeamDeletion": false,
|
"EnableAPITeamDeletion": false,
|
||||||
|
"EnableAPITriggerAdminNotifications": false,
|
||||||
"ExperimentalEnableHardenedMode": false
|
"ExperimentalEnableHardenedMode": false
|
||||||
},
|
},
|
||||||
"TeamSettings": {
|
"TeamSettings": {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user