[MM-43649] - Ability for end users to notify admin to upgrade workspace (#20338)
* [MM-43649] - Ability for end users to notify admin to upgrade workspace * add test case * move code to app layer * feedback impl * feedback impl-1 * feedback impl * simplify logic * fix typo and lint * fix app layers * Use a single critical section for users notified admin (#20488) * Use a single critical section for users notifying admin * change persistence mechanism * add more tests * change bot message * fix translations * update logic to notify once within cooloff period Co-authored-by: Mattermod <mattermod@users.noreply.github.com> Co-authored-by: Nathaniel Allred <neallred@protonmail.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
dad8eab777
Коммит
1d9fa3c333
@@ -48,6 +48,25 @@ 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.SetInvalidParam("notifyAdminRequest")
|
||||||
|
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,9 +5,12 @@ package api4
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -295,6 +298,108 @@ func Test_requestTrial(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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("Initial request has invalid email", func(t *testing.T) {
|
t.Run("Initial request has invalid email", func(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
|
|||||||
@@ -890,6 +890,7 @@ 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.Context, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError)
|
PatchChannel(c *request.Context, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError)
|
||||||
|
|||||||
93
app/cloud.go
93
app/cloud.go
@@ -4,15 +4,108 @@
|
|||||||
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/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]interface{}{"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
|
||||||
|
}
|
||||||
|
|
||||||
type cloudWrapper struct {
|
type cloudWrapper struct {
|
||||||
cloud einterfaces.CloudInterface
|
cloud einterfaces.CloudInterface
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12280,6 +12280,28 @@ 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")
|
||||||
|
|||||||
@@ -475,6 +475,10 @@
|
|||||||
"id": "api.cloud.license_error",
|
"id": "api.cloud.license_error",
|
||||||
"translation": "Your license does not support cloud requests."
|
"translation": "Your license does not support cloud requests."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.cloud.notify_admin_to_upgrade_error.already_notified",
|
||||||
|
"translation": "Already notified admin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.cloud.request_error",
|
"id": "api.cloud.request_error",
|
||||||
"translation": "Error processing request to CWS."
|
"translation": "Error processing request to CWS."
|
||||||
@@ -491,6 +495,10 @@
|
|||||||
"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."
|
||||||
|
|||||||
@@ -7915,6 +7915,22 @@ func (c *Client4) RequestCloudTrial(email *StartCloudTrialRequest) (*Subscriptio
|
|||||||
return subscription, BuildResponse(r), nil
|
return subscription, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client4) NotifyAdmin(nr *NotifyAdminToUpgradeRequest) int {
|
||||||
|
nrJSON, jsonErr := json.Marshal(nr)
|
||||||
|
if jsonErr != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.DoAPIPost(c.cloudRoute()+"/notify-admin-to-upgrade", string(nrJSON))
|
||||||
|
if err != nil {
|
||||||
|
return r.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBody(r)
|
||||||
|
|
||||||
|
return r.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client4) ValidateBusinessEmail(email *ValidateBusinessEmailRequest) (*Response, error) {
|
func (c *Client4) ValidateBusinessEmail(email *ValidateBusinessEmailRequest) (*Response, error) {
|
||||||
payload, _ := json.Marshal(email)
|
payload, _ := json.Marshal(email)
|
||||||
r, err := c.DoAPIPostBytes(c.cloudRoute()+"/validate-business-email", payload)
|
r, err := c.DoAPIPostBytes(c.cloudRoute()+"/validate-business-email", payload)
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EventTypeFailedPayment = "failed-payment"
|
EventTypeFailedPayment = "failed-payment"
|
||||||
@@ -37,6 +41,9 @@ 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"`
|
||||||
@@ -228,3 +235,23 @@ 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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const (
|
|||||||
PreferenceEmailIntervalFifteenAsSeconds = "900"
|
PreferenceEmailIntervalFifteenAsSeconds = "900"
|
||||||
PreferenceEmailIntervalHour = "hour"
|
PreferenceEmailIntervalHour = "hour"
|
||||||
PreferenceEmailIntervalHourAsSeconds = "3600"
|
PreferenceEmailIntervalHourAsSeconds = "3600"
|
||||||
|
PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Preference struct {
|
type Preference struct {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user