diff --git a/api4/system.go b/api4/system.go
index f17667ae79..ac754a9cd3 100644
--- a/api4/system.go
+++ b/api4/system.go
@@ -54,6 +54,9 @@ func (api *API) InitSystem() {
api.BaseRoutes.ApiRoot.Handle("/server_busy", api.ApiSessionRequired(setServerBusy)).Methods("POST")
api.BaseRoutes.ApiRoot.Handle("/server_busy", api.ApiSessionRequired(getServerBusyExpires)).Methods("GET")
api.BaseRoutes.ApiRoot.Handle("/server_busy", api.ApiSessionRequired(clearServerBusy)).Methods("DELETE")
+
+ api.BaseRoutes.ApiRoot.Handle("/warn_metrics/status", api.ApiSessionRequired(getWarnMetricsStatus)).Methods("GET")
+ api.BaseRoutes.ApiRoot.Handle("/warn_metrics/ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.ApiHandler(sendWarnMetricAckEmail)).Methods("POST")
}
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -544,3 +547,61 @@ func getServerBusyExpires(c *Context, w http.ResponseWriter, r *http.Request) {
}
w.Write([]byte(c.App.Srv().Busy.ToJson()))
}
+
+func getWarnMetricsStatus(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ license := c.App.Srv().License()
+ if license != nil {
+ mlog.Debug("License is present, skip.")
+ return
+ }
+
+ status, err := c.App.GetWarnMetricsStatus()
+ if err != nil {
+ c.Err = err
+ return
+ }
+
+ w.Write([]byte(model.MapWarnMetricStatusToJson(status)))
+}
+
+func sendWarnMetricAckEmail(c *Context, w http.ResponseWriter, r *http.Request) {
+ auditRec := c.MakeAuditRecord("sendWarnMetricAckEmail", audit.Fail)
+ defer c.LogAuditRec(auditRec)
+ c.LogAudit("attempt")
+
+ if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+
+ license := c.App.Srv().License()
+ if license != nil {
+ mlog.Debug("License is present, skip.")
+ return
+ }
+
+ user, appErr := c.App.GetUser(c.App.Session().UserId)
+ if appErr != nil {
+ c.Err = appErr
+ return
+ }
+
+ ack := model.SendWarnMetricAckFromJson(r.Body)
+ if ack == nil {
+ c.SetInvalidParam("ack")
+ return
+ }
+
+ appErr = c.App.NotifyAndSetWarnMetricAck(c.Params.WarnMetricId, user, ack.ForceAck, false)
+ if appErr != nil {
+ c.Err = appErr
+ }
+
+ auditRec.Success()
+ ReturnStatusOK(w)
+}
diff --git a/app/admin.go b/app/admin.go
index 20a57c3004..9e3191e388 100644
--- a/app/admin.go
+++ b/app/admin.go
@@ -221,7 +221,7 @@ func (a *App) TestEmail(userId string, cfg *model.Config) *model.AppError {
T := utils.GetUserTranslations(user.Locale)
license := a.Srv().License()
- if err := mailservice.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg, license != nil && *license.Features.Compliance); err != nil {
+ if err := mailservice.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg, license != nil && *license.Features.Compliance, ""); err != nil {
return model.NewAppError("testEmail", "app.admin.test_email.failure", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError)
}
diff --git a/app/app.go b/app/app.go
index 706db11ba1..a5d27c4c96 100644
--- a/app/app.go
+++ b/app/app.go
@@ -5,16 +5,20 @@ package app
import (
"context"
+ "fmt"
"html/template"
"net/http"
"strconv"
+ "strings"
+ "github.com/mattermost/go-i18n/i18n"
goi18n "github.com/mattermost/go-i18n/i18n"
"github.com/mattermost/mattermost-server/v5/einterfaces"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/services/httpservice"
"github.com/mattermost/mattermost-server/v5/services/imageproxy"
+ "github.com/mattermost/mattermost-server/v5/services/mailservice"
"github.com/mattermost/mattermost-server/v5/services/searchengine"
"github.com/mattermost/mattermost-server/v5/services/timezones"
"github.com/mattermost/mattermost-server/v5/utils"
@@ -107,6 +111,7 @@ func (a *App) InitServer() {
if a.Srv().runjobs {
a.Srv().Go(func() {
runLicenseExpirationCheckJob(a)
+ runCheckNumberOfActiveUsersWarnMetricStatusJob(a)
})
}
a.srv.RunJobs()
@@ -184,6 +189,253 @@ func (s *Server) getFirstServerRunTimestamp() (int64, *model.AppError) {
return value, nil
}
+func (a *App) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
+ systemDataList, appErr := a.Srv().Store.System().Get()
+ if appErr != nil {
+ return nil, appErr
+ }
+
+ result := map[string]*model.WarnMetricStatus{}
+ for key, value := range systemDataList {
+ if strings.HasPrefix(key, model.WARN_METRIC_STATUS_STORE_PREFIX) {
+ if warnMetric, ok := model.WarnMetricsTable[key]; ok {
+ if !warnMetric.IsBotOnly && value == model.WARN_METRIC_STATUS_LIMIT_REACHED {
+ result[key], _ = a.getWarnMetricStatusAndDisplayTextsForId(key, nil)
+ }
+ }
+ }
+ }
+
+ return result, nil
+}
+
+func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18n.TranslateFunc) (*model.WarnMetricStatus, *model.WarnMetricDisplayTexts) {
+ var warnMetricStatus *model.WarnMetricStatus
+ var warnMetricDisplayTexts = &model.WarnMetricDisplayTexts{}
+
+ if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
+ warnMetricStatus = &model.WarnMetricStatus{
+ Id: warnMetric.Id,
+ Limit: warnMetric.Limit,
+ Acked: false,
+ }
+
+ if T == nil {
+ mlog.Debug("No translation function")
+ return warnMetricStatus, nil
+ }
+
+ warnMetricDisplayTexts.BotMailToBody = T("api.server.warn_metric.bot_response.number_of_users.mailto_body", map[string]interface{}{"Limit": warnMetric.Limit})
+ warnMetricDisplayTexts.EmailBody = T("api.templates.warn_metric_ack.number_of_active_users.body", map[string]interface{}{"Limit": warnMetric.Limit})
+
+ switch warnMetricId {
+ case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200:
+ warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_200.notification_title")
+ warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.notification_body")
+ case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400:
+ warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_400.notification_title")
+ warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_400.notification_body")
+ case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500:
+ warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_500.notification_title")
+ warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.notification_body")
+ default:
+ mlog.Error("Invalid metric id", mlog.String("id", warnMetricId))
+ return nil, nil
+ }
+
+ return warnMetricStatus, warnMetricDisplayTexts
+ }
+ return nil, nil
+}
+
+func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppError {
+ perPage := 25
+ userOptions := &model.UserGetOptions{
+ Page: 0,
+ PerPage: perPage,
+ Role: model.SYSTEM_ADMIN_ROLE_ID,
+ Inactive: false,
+ }
+
+ // get sysadmins
+ var sysAdmins []*model.User
+ for {
+ sysAdminsList, err := a.GetUsers(userOptions)
+ if err != nil {
+ return err
+ }
+
+ if len(sysAdminsList) == 0 {
+ return model.NewAppError("NotifyAdminsOfWarnMetricStatus", "app.system.warn_metric.notification.empty_admin_list.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ sysAdmins = append(sysAdmins, sysAdminsList...)
+
+ if len(sysAdminsList) < perPage {
+ mlog.Debug("Number of system admins is less than page limit", mlog.Int("count", len(sysAdminsList)))
+ break
+ }
+ }
+
+ T := utils.GetUserTranslations(sysAdmins[0].Locale)
+ warnMetricsBot := &model.Bot{
+ Username: model.BOT_WARN_METRIC_BOT_USERNAME,
+ DisplayName: T("app.system.warn_metric.bot_displayname"),
+ Description: "",
+ OwnerId: sysAdmins[0].Id,
+ }
+
+ bot, err := a.getOrCreateWarnMetricsBot(warnMetricsBot)
+ if err != nil {
+ return err
+ }
+
+ for _, sysAdmin := range sysAdmins {
+ T := utils.GetUserTranslations(sysAdmin.Locale)
+ bot.DisplayName = T("app.system.warn_metric.bot_displayname")
+ bot.Description = T("app.system.warn_metric.bot_description")
+
+ channel, appErr := a.GetOrCreateDirectChannel(bot.UserId, sysAdmin.Id)
+ if appErr != nil {
+ mlog.Error("Cannot create channel for system bot notification!", mlog.String("Admin Id", sysAdmin.Id))
+ return appErr
+ }
+
+ warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T)
+ if warnMetricStatus == nil {
+ return model.NewAppError("NotifyAdminsOfWarnMetricStatus", "app.system.warn_metric.notification.invalid_metric.app_error", nil, "", http.StatusInternalServerError)
+ }
+
+ botPost := &model.Post{
+ UserId: bot.UserId,
+ ChannelId: channel.Id,
+ Type: model.POST_SYSTEM_WARN_METRIC_STATUS,
+ Message: "",
+ }
+
+ actions := []*model.PostAction{}
+ actions = append(actions,
+ &model.PostAction{
+ Id: "contactUs",
+ Name: T("api.server.warn_metric.contact_us"),
+ Type: model.POST_ACTION_TYPE_BUTTON,
+ Options: []*model.PostActionOptions{
+ {
+ Text: "TrackEventId",
+ Value: warnMetricId,
+ },
+ {
+ Text: "ActionExecutingMessage",
+ Value: T("api.server.warn_metric.contacting_us"),
+ },
+ },
+ Integration: &model.PostActionIntegration{
+ Context: model.StringInterface{
+ "bot_user_id": bot.UserId,
+ "force_ack": false,
+ },
+ URL: fmt.Sprintf("/warn_metrics/ack/%s", warnMetricId),
+ },
+ },
+ )
+
+ attachments := []*model.SlackAttachment{{
+ AuthorName: "",
+ Title: warnMetricDisplayTexts.BotTitle,
+ Text: warnMetricDisplayTexts.BotMessageBody,
+ Actions: actions,
+ }}
+ model.ParseSlackAttachment(botPost, attachments)
+
+ mlog.Debug("Send admin advisory for metric", mlog.String("warnMetricId", warnMetricId), mlog.String("userid", botPost.UserId))
+ if _, err := a.CreatePostAsUser(botPost, a.Session().Id, true); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
+ if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
+ data, err := a.Srv().Store.System().GetByName(warnMetric.Id)
+ if err == nil && data != nil && data.Value == model.WARN_METRIC_STATUS_ACK {
+ mlog.Debug("This metric warning has already been acknowledged")
+ return nil
+ }
+
+ if !forceAck {
+ if len(*a.Config().EmailSettings.SMTPServer) == 0 {
+ return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.missing_server.app_error", nil, utils.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}), http.StatusInternalServerError)
+ }
+ T := utils.GetUserTranslations(sender.Locale)
+ bodyPage := a.Srv().EmailService.newEmailTemplate("warn_metric_ack", sender.Locale)
+ bodyPage.Props["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header")
+ bodyPage.Props["ContactNameValue"] = sender.GetFullName()
+ bodyPage.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header")
+ bodyPage.Props["ContactEmailValue"] = sender.Email
+
+ //same definition as the active users count metric displayed in the SystemConsole Analytics section
+ registeredUsersCount, cerr := a.Srv().Store.User().Count(model.UserCountOptions{})
+ if cerr != nil {
+ mlog.Error("Error retrieving the number of registered users", mlog.Err(cerr))
+ } else {
+ bodyPage.Props["RegisteredUsersHeader"] = T("api.templates.warn_metric_ack.body.registered_users_header")
+ bodyPage.Props["RegisteredUsersValue"] = registeredUsersCount
+ }
+ bodyPage.Props["SiteURLHeader"] = T("api.templates.warn_metric_ack.body.site_url_header")
+ bodyPage.Props["SiteURL"] = a.GetSiteURL()
+ bodyPage.Props["DiagnosticIdHeader"] = T("api.templates.warn_metric_ack.body.diagnostic_id_header")
+ bodyPage.Props["DiagnosticIdValue"] = a.DiagnosticId()
+ bodyPage.Props["Footer"] = T("api.templates.warn_metric_ack.footer")
+
+ warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T)
+ if warnMetricStatus == nil {
+ return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", nil, "", http.StatusInternalServerError)
+ }
+
+ subject := T("api.templates.warn_metric_ack.subject")
+ bodyPage.Props["Title"] = warnMetricDisplayTexts.EmailBody
+
+ if err = mailservice.SendMailUsingConfig(model.MM_SUPPORT_ADDRESS, subject, bodyPage.Render(), a.Config(), false, sender.Email); err != nil {
+ mlog.Error("Error while sending email", mlog.String("destination email", model.MM_SUPPORT_ADDRESS), mlog.Err(err))
+ return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError)
+ }
+ }
+
+ mlog.Debug("Disable the monitoring of all warn metrics")
+ err = a.setWarnMetricsStatus(model.WARN_METRIC_STATUS_ACK)
+ if err != nil {
+ return err
+ }
+
+ if !warnMetric.IsBotOnly && !isBot {
+ message := model.NewWebSocketEvent(model.WEBSOCKET_WARN_METRIC_STATUS_REMOVED, "", "", "", nil)
+ message.Add("warnMetricId", warnMetric.Id)
+ a.Publish(message)
+ }
+ }
+ return nil
+}
+
+func (a *App) setWarnMetricsStatus(status string) *model.AppError {
+ for _, warnMetric := range model.WarnMetricsTable {
+ a.setWarnMetricsStatusForId(warnMetric.Id, status)
+ }
+ return nil
+}
+
+func (a *App) setWarnMetricsStatusForId(warnMetricId string, status string) *model.AppError {
+ mlog.Info("Storing user acknowledgement for warn metric", mlog.String("warnMetricId", warnMetricId))
+ if err := a.Srv().Store.System().SaveOrUpdate(&model.System{
+ Name: warnMetricId,
+ Value: status,
+ }); err != nil {
+ mlog.Error("Unable to write to database.", mlog.Err(err))
+ return model.NewAppError("setWarnMetricsStatusForId", "app.system.warn_metric.store.app_error", map[string]interface{}{"WarnMetricName": warnMetricId}, "", http.StatusInternalServerError)
+ }
+ return nil
+}
+
func (a *App) Srv() *Server {
return a.srv
}
diff --git a/app/app_iface.go b/app/app_iface.go
index b46934d2c6..66afab35b8 100644
--- a/app/app_iface.go
+++ b/app/app_iface.go
@@ -692,6 +692,7 @@ type AppIface interface {
GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError)
GetVerifyEmailToken(token string) (*model.Token, *model.AppError)
GetViewUsersRestrictions(userId string) (*model.ViewUsersRestrictions, *model.AppError)
+ GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError)
HTTPService() httpservice.HTTPService
Handle404(w http.ResponseWriter, r *http.Request)
HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError)
@@ -753,6 +754,7 @@ type AppIface interface {
NewPluginAPI(manifest *model.Manifest) plugin.API
Notification() einterfaces.NotificationInterface
NotificationsLog() *mlog.Logger
+ NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError
OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError
OriginChecker() func(*http.Request) bool
PatchChannel(channel *model.Channel, patch *model.ChannelPatch, userId string) (*model.Channel, *model.AppError)
diff --git a/app/bot.go b/app/bot.go
index 9b53a4e5da..6f22f34a57 100644
--- a/app/bot.go
+++ b/app/bot.go
@@ -65,6 +65,51 @@ func (a *App) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
return savedBot, nil
}
+func (a *App) getOrCreateWarnMetricsBot(botDef *model.Bot) (*model.Bot, *model.AppError) {
+ botUser, appErr := a.GetUserByUsername(botDef.Username)
+ if appErr != nil {
+ if appErr.StatusCode != http.StatusNotFound {
+ mlog.Error(appErr.Error())
+ return nil, appErr
+ }
+
+ // cannot find this bot user, save the user
+ user, err := a.Srv().Store.User().Save(model.UserFromBot(botDef))
+ if err != nil {
+ mlog.Error(err.Error())
+ return nil, err
+ }
+ botDef.UserId = user.Id
+
+ //save the bot
+ savedBot, nErr := a.Srv().Store.Bot().Save(botDef)
+ if nErr != nil {
+ a.Srv().Store.User().PermanentDelete(savedBot.UserId)
+ var nAppErr *model.AppError
+ switch {
+ case errors.As(nErr, &nAppErr): // in case we haven't converted to plain error.
+ return nil, nAppErr
+ default: // last fallback in case it doesn't map to an existing app error.
+ return nil, model.NewAppError("getOrCreateWarnMetricsBot", "app.bot.createbot.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
+ }
+ }
+ return savedBot, nil
+ }
+
+ if botUser == nil {
+ return nil, model.NewAppError("getOrCreateWarnMetricsBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError)
+ }
+
+ //return the bot for this user
+ savedBot, appErr := a.GetBot(botUser.Id, false)
+ if appErr != nil {
+ mlog.Error(appErr.Error())
+ return nil, appErr
+ }
+
+ return savedBot, nil
+}
+
// PatchBot applies the given patch to the bot and corresponding user.
func (a *App) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
bot, err := a.GetBot(botUserId, true)
diff --git a/app/diagnostics.go b/app/diagnostics.go
index 6e6bbc7f67..6a701d38a8 100644
--- a/app/diagnostics.go
+++ b/app/diagnostics.go
@@ -57,6 +57,7 @@ const (
TRACK_ELASTICSEARCH = "elasticsearch"
TRACK_GROUPS = "groups"
TRACK_CHANNEL_MODERATION = "channel_moderation"
+ TRACK_WARN_METRICS = "warn_metrics"
TRACK_ACTIVITY = "activity"
TRACK_LICENSE = "license"
@@ -83,6 +84,7 @@ func (s *Server) sendDailyDiagnostics(override bool) {
s.trackElasticsearch()
s.trackGroups()
s.trackChannelModeration()
+ s.trackWarnMetrics()
}
}
@@ -1088,3 +1090,19 @@ func (s *Server) trackChannelModeration() {
"use_channel_mentions_guest_disabled_count": useChannelMentionsGuest,
})
}
+
+func (s *Server) trackWarnMetrics() {
+ systemDataList, appErr := s.Store.System().Get()
+ if appErr != nil {
+ return
+ }
+ for key, value := range systemDataList {
+ if strings.HasPrefix(key, model.WARN_METRIC_STATUS_STORE_PREFIX) {
+ if _, ok := model.WarnMetricsTable[key]; ok {
+ s.SendDiagnostic(TRACK_WARN_METRICS, map[string]interface{}{
+ key: value != "false",
+ })
+ }
+ }
+ }
+}
diff --git a/app/email.go b/app/email.go
index c358b79d74..f4413f187e 100644
--- a/app/email.go
+++ b/app/email.go
@@ -550,15 +550,19 @@ func (es *EmailService) sendNotificationMail(to, subject, htmlBody string) *mode
}
func (es *EmailService) sendMail(to, subject, htmlBody string) *model.AppError {
+ return es.sendMailWithCC(to, subject, htmlBody, "")
+}
+
+func (es *EmailService) sendMailWithCC(to, subject, htmlBody string, ccMail string) *model.AppError {
license := es.srv.License()
- return mailservice.SendMailUsingConfig(to, subject, htmlBody, es.srv.Config(), license != nil && *license.Features.Compliance)
+ return mailservice.SendMailUsingConfig(to, subject, htmlBody, es.srv.Config(), license != nil && *license.Features.Compliance, ccMail)
}
func (es *EmailService) sendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader) *model.AppError {
license := es.srv.License()
config := es.srv.Config()
- return mailservice.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, config, license != nil && *license.Features.Compliance)
+ return mailservice.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, config, license != nil && *license.Features.Compliance, "")
}
func (es *EmailService) CreateVerifyEmailToken(userId string, newEmail string) (*model.Token, *model.AppError) {
diff --git a/app/integration_action.go b/app/integration_action.go
index cf4e027d52..7e44e09c42 100644
--- a/app/integration_action.go
+++ b/app/integration_action.go
@@ -26,10 +26,12 @@ import (
"net/http"
"net/url"
"path"
+ "path/filepath"
"strings"
"github.com/gorilla/mux"
+ "github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/mattermost/mattermost-server/v5/utils"
@@ -211,11 +213,20 @@ func (a *App) DoPostActionWithCookie(postId, actionId, userId, selectedOption st
return "", appErr
}
- resp, appErr := a.DoActionRequest(upstreamURL, upstreamRequest.ToJson())
- if appErr != nil {
- return "", appErr
+ var resp *http.Response
+ if strings.HasPrefix(upstreamURL, "/warn_metrics/") {
+ appErr = a.doLocalWarnMetricsRequest(upstreamURL, upstreamRequest)
+ if appErr != nil {
+ return "", appErr
+ }
+ return "", nil
+ } else {
+ resp, appErr = a.DoActionRequest(upstreamURL, upstreamRequest.ToJson())
+ if appErr != nil {
+ return "", appErr
+ }
+ defer resp.Body.Close()
}
- defer resp.Body.Close()
var response model.PostActionIntegrationResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
@@ -398,6 +409,129 @@ func (a *App) doPluginRequest(method, rawURL string, values url.Values, body []b
return resp, nil
}
+func (a *App) doLocalWarnMetricsRequest(rawURL string, upstreamRequest *model.PostActionIntegrationRequest) *model.AppError {
+ _, err := url.Parse(rawURL)
+ if err != nil {
+ return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, err.Error(), http.StatusBadRequest)
+ }
+
+ warnMetricId := filepath.Base(rawURL)
+ if warnMetricId == "" {
+ return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest)
+ }
+
+ user, appErr := a.GetUser(a.Session().UserId)
+ if appErr != nil {
+ return appErr
+ }
+
+ botPost := &model.Post{
+ UserId: upstreamRequest.Context["bot_user_id"].(string),
+ ChannelId: upstreamRequest.ChannelId,
+ HasReactions: true,
+ }
+
+ forceAck := upstreamRequest.Context["force_ack"].(bool)
+
+ if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, user, forceAck, true); appErr != nil {
+ if forceAck {
+ return appErr
+ }
+ mailtoLinkText := a.buildWarnMetricMailtoLink(warnMetricId, user)
+ botPost.Message = ":warning: " + utils.T("api.server.warn_metric.bot_response.notification_failure.message")
+ actions := []*model.PostAction{}
+ actions = append(actions,
+ &model.PostAction{
+ Id: "emailUs",
+ Name: utils.T("api.server.warn_metric.email_us"),
+ Type: model.POST_ACTION_TYPE_BUTTON,
+ Options: []*model.PostActionOptions{
+ {
+ Text: "WarnMetricMailtoUrl",
+ Value: mailtoLinkText,
+ },
+ {
+ Text: "TrackEventId",
+ Value: warnMetricId,
+ },
+ },
+ Integration: &model.PostActionIntegration{
+ Context: model.StringInterface{
+ "bot_user_id": botPost.UserId,
+ "force_ack": true,
+ },
+ URL: fmt.Sprintf("/warn_metrics/ack/%s", model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500),
+ },
+ },
+ )
+ attachements := []*model.SlackAttachment{{
+ AuthorName: "",
+ Title: "",
+ Actions: actions,
+ Text: utils.T("api.server.warn_metric.bot_response.notification_failure.body"),
+ }}
+ model.ParseSlackAttachment(botPost, attachements)
+ } else {
+ botPost.Message = ":white_check_mark: " + utils.T("api.server.warn_metric.bot_response.notification_success.message")
+ }
+
+ if _, err := a.CreatePostAsUser(botPost, a.Session().Id, true); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+type MailToLinkContent struct {
+ MetricId string `json:"metric_id"`
+ MailRecipient string `json:"mail_recipient"`
+ MailCC string `json:"mail_cc"`
+ MailSubject string `json:"mail_subject"`
+ MailBody string `json:"mail_body"`
+}
+
+func (mlc *MailToLinkContent) ToJson() string {
+ b, _ := json.Marshal(mlc)
+ return string(b)
+}
+
+func (a *App) buildWarnMetricMailtoLink(warnMetricId string, user *model.User) string {
+ T := utils.GetUserTranslations(user.Locale)
+ _, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T)
+
+ mailBody := warnMetricDisplayTexts.BotMailToBody
+ mailBody += T("api.server.warn_metric.bot_response.mailto_contact_header", map[string]interface{}{"Contact": user.GetFullName()})
+ mailBody += "\r\n"
+ mailBody += T("api.server.warn_metric.bot_response.mailto_email_header", map[string]interface{}{"Email": user.Email})
+ mailBody += "\r\n"
+
+ registeredUsersCount, err := a.Srv().Store.User().Count(model.UserCountOptions{})
+ if err != nil {
+ mlog.Error("Error retrieving the number of registered users", mlog.Err(err))
+ } else {
+ mailBody += utils.T("api.server.warn_metric.bot_response.mailto_registered_users_header", map[string]interface{}{"NoRegisteredUsers": registeredUsersCount})
+ mailBody += "\r\n"
+ }
+
+ mailBody += T("api.server.warn_metric.bot_response.mailto_site_url_header", map[string]interface{}{"SiteUrl": a.GetSiteURL()})
+ mailBody += "\r\n"
+
+ mailBody += T("api.server.warn_metric.bot_response.mailto_diagnostic_id_header", map[string]interface{}{"DiagnosticId": a.DiagnosticId()})
+ mailBody += "\r\n"
+
+ mailBody += T("api.server.warn_metric.bot_response.mailto_footer")
+
+ mailToLinkContent := &MailToLinkContent{
+ MetricId: warnMetricId,
+ MailRecipient: "support@mattermost.com",
+ MailCC: user.Email,
+ MailSubject: T("api.server.warn_metric.bot_response.mailto_subject"),
+ MailBody: mailBody,
+ }
+
+ return mailToLinkContent.ToJson()
+}
+
func (a *App) DoLocalRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
return a.doPluginRequest("POST", rawURL, nil, body)
}
diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go
index 10a044f339..6a285a3fef 100644
--- a/app/opentracing/opentracing_layer.go
+++ b/app/opentracing/opentracing_layer.go
@@ -8940,6 +8940,28 @@ func (a *OpenTracingAppLayer) GetViewUsersRestrictions(userId string) (*model.Vi
return resultVar0, resultVar1
}
+func (a *OpenTracingAppLayer) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
+ origCtx := a.ctx
+ span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWarnMetricsStatus")
+
+ 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.GetWarnMetricsStatus()
+
+ if resultVar1 != nil {
+ span.LogFields(spanlog.Error(resultVar1))
+ ext.Error.Set(span, true)
+ }
+
+ return resultVar0, resultVar1
+}
+
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
@@ -10255,6 +10277,28 @@ func (a *OpenTracingAppLayer) NewWebHub() *app.Hub {
return resultVar0
}
+func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
+ origCtx := a.ctx
+ span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifyAndSetWarnMetricAck")
+
+ 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.NotifyAndSetWarnMetricAck(warnMetricId, sender, forceAck, isBot)
+
+ if resultVar0 != nil {
+ span.LogFields(spanlog.Error(resultVar0))
+ ext.Error.Set(span, true)
+ }
+
+ return resultVar0
+}
+
func (a *OpenTracingAppLayer) NotifySessionsExpired() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySessionsExpired")
diff --git a/app/security_update_check.go b/app/security_update_check.go
index 071d00e7ff..d918623614 100644
--- a/app/security_update_check.go
+++ b/app/security_update_check.go
@@ -115,7 +115,7 @@ func (s *Server) DoSecurityUpdateCheck() {
for _, user := range users {
mlog.Info("Sending security bulletin", mlog.String("bulletin_id", bulletin.Id), mlog.String("user_email", user.Email))
license := s.License()
- mailservice.SendMailUsingConfig(user.Email, utils.T("mattermost.bulletin.subject"), string(body), s.Config(), license != nil && *license.Features.Compliance)
+ mailservice.SendMailUsingConfig(user.Email, utils.T("mattermost.bulletin.subject"), string(body), s.Config(), license != nil && *license.Features.Compliance, "")
}
bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id}
diff --git a/app/server.go b/app/server.go
index 94b9ad4c0b..1659fc7ee3 100644
--- a/app/server.go
+++ b/app/server.go
@@ -1090,6 +1090,13 @@ func runLicenseExpirationCheckJob(a *App) {
}, time.Hour*24)
}
+func runCheckNumberOfActiveUsersWarnMetricStatusJob(a *App) {
+ doCheckNumberOfActiveUsersWarnMetricStatus(a)
+ model.CreateRecurringTask("Check Number Of Active Users Warn Metric Status", func() {
+ doCheckNumberOfActiveUsersWarnMetricStatus(a)
+ }, time.Hour*24)
+}
+
func doSecurity(s *Server) {
s.DoSecurityUpdateCheck()
}
@@ -1117,6 +1124,58 @@ func doSessionCleanup(s *Server) {
s.Store.Session().Cleanup(model.GetMillis(), SESSIONS_CLEANUP_BATCH_SIZE)
}
+func doCheckNumberOfActiveUsersWarnMetricStatus(a *App) {
+ license := a.Srv().License()
+ if license != nil {
+ mlog.Debug("License is present, skip this check")
+ return
+ }
+
+ numberOfActiveUsers, err := a.Srv().Store.User().Count(model.UserCountOptions{})
+ if err != nil {
+ mlog.Error("Error to get active registered users.", mlog.Err(err))
+ }
+
+ warnMetrics := []model.WarnMetric{}
+ if numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit {
+ return
+ } else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400].Limit {
+ warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200])
+ } else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500].Limit {
+ warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400])
+ } else {
+ warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500])
+ }
+
+ for _, warnMetric := range warnMetrics {
+ data, err := a.Srv().Store.System().GetByName(warnMetric.Id)
+ if err == nil && data != nil && (data.Value == model.WARN_METRIC_STATUS_ACK || data.Value == model.WARN_METRIC_STATUS_RUNONCE) {
+ mlog.Debug("This metric warning has already been acked or should only run once")
+ continue
+ }
+
+ if err = a.Srv().Store.System().SaveOrUpdate(&model.System{Name: warnMetric.Id, Value: model.WARN_METRIC_STATUS_LIMIT_REACHED}); err != nil {
+ mlog.Error("Unable to write to database.", mlog.String("id", warnMetric.Id), mlog.Err(err))
+ continue
+ }
+ warnMetricStatus, _ := a.getWarnMetricStatusAndDisplayTextsForId(warnMetric.Id, nil)
+
+ if !warnMetric.IsBotOnly {
+ message := model.NewWebSocketEvent(model.WEBSOCKET_WARN_METRIC_STATUS_RECEIVED, "", "", "", nil)
+ message.Add("warnMetricStatus", warnMetricStatus.ToJson())
+ a.Publish(message)
+ }
+
+ if err = a.notifyAdminsOfWarnMetricStatus(warnMetric.Id); err != nil {
+ mlog.Error("Failed to send notifications to admin users.", mlog.Err(err))
+ }
+
+ if warnMetric.IsRunOnce {
+ a.setWarnMetricsStatusForId(warnMetric.Id, model.WARN_METRIC_STATUS_RUNONCE)
+ }
+ }
+}
+
func doLicenseExpirationCheck(a *App) {
a.Srv().LoadLicense()
license := a.Srv().License()
diff --git a/i18n/en.json b/i18n/en.json
index f334940ec1..3af8210b50 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1178,6 +1178,18 @@
"id": "api.create_terms_of_service.empty_text.app_error",
"translation": "Please enter text for your Custom Terms of Service."
},
+ {
+ "id": "api.email.send_warn_metric_ack.failure.app_error",
+ "translation": "Failure to send admin acknowledgment email"
+ },
+ {
+ "id": "api.email.send_warn_metric_ack.invalid_warn_metric.app_error",
+ "translation": "Could not find warn metric."
+ },
+ {
+ "id": "api.email.send_warn_metric_ack.missing_server.app_error",
+ "translation": "SMTP Server is required"
+ },
{
"id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error",
"translation": "Email batching job's receiving channel was full. Please increase the EmailBatchingBufferSize."
@@ -1926,6 +1938,86 @@
"id": "api.server.start_server.starting.critical",
"translation": "Error starting server, err:%v"
},
+ {
+ "id": "api.server.warn_metric.bot_response.mailto_contact_header",
+ "translation": "Contact: {{.Contact}}"
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.mailto_diagnostic_id_header",
+ "translation": "Diagnostic Id: {{.DiagnosticId}}"
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.mailto_email_header",
+ "translation": "Email: {{.Email}}"
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.mailto_footer",
+ "translation": "If you have any additional inquiries, please contact support@mattermost.com"
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.mailto_registered_users_header",
+ "translation": "Total Active Users: {{.NoRegisteredUsers}}"
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.mailto_site_url_header",
+ "translation": "Site URL: {{.SiteUrl}}"
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.mailto_subject",
+ "translation": "Mattermost Contact Us request"
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.notification_failure.body",
+ "translation": "Please email us."
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.notification_failure.message",
+ "translation": "Message could not be sent."
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.notification_success.message",
+ "translation": "Thank you for contacting Mattermost. We will follow up with you soon."
+ },
+ {
+ "id": "api.server.warn_metric.bot_response.number_of_users.mailto_body",
+ "translation": "Mattermost Contact Us request. My team has now {{.Limit}} users and I am considering Mattermost Enterprise Edition.\r\n"
+ },
+ {
+ "id": "api.server.warn_metric.contact_us",
+ "translation": "Contact us"
+ },
+ {
+ "id": "api.server.warn_metric.contacting_us",
+ "translation": "Contacting us"
+ },
+ {
+ "id": "api.server.warn_metric.email_us",
+ "translation": "Email us"
+ },
+ {
+ "id": "api.server.warn_metric.number_of_active_users_200.notification_body",
+ "translation": "Your Mattermost system now has 200 users. As your user base grows, provisioning new accounts can become time-consuming. We recommend that you upgrade to Mattermost Enterprise E10 and integrate your organization’s Active Directory/LDAP, which will allow anyone with an account to access Mattermost. Users can log in without having to create new usernames and passwords, and administrators save time provisioning and managing accounts.\n[Learn more about integrating with AD/LDAP](https://docs.mattermost.com/deployment/sso-ldap.html?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)"
+ },
+ {
+ "id": "api.server.warn_metric.number_of_active_users_200.notification_title",
+ "translation": "Integrate AD/LDAP"
+ },
+ {
+ "id": "api.server.warn_metric.number_of_active_users_400.notification_body",
+ "translation": "Your Mattermost system now has 400 users. When you connect Mattermost with your organization's single sign-on provider, users can access Mattermost without having to re-enter their credentials. Contact support to learn more about integrating with SAML 2.0, available in Mattermost Enterprise E20.\n[Learn more about integrating with SAML 2.0](https://docs.mattermost.com/deployment/sso-saml.html?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml)"
+ },
+ {
+ "id": "api.server.warn_metric.number_of_active_users_400.notification_title",
+ "translation": "Integrate SAML 2.0"
+ },
+ {
+ "id": "api.server.warn_metric.number_of_active_users_500.notification_body",
+ "translation": "Mattermost strongly recommends that deployments of over 500 users upgrade to Mattermost Enterprise E20, which offers features such as user management, server clustering, and performance monitoring."
+ },
+ {
+ "id": "api.server.warn_metric.number_of_active_users_500.notification_title",
+ "translation": "Upgrade to Mattermost Enterprise edition"
+ },
{
"id": "api.slackimport.slack_add_bot_user.email_pwd",
"translation": "The Integration/Slack Bot user with email {{.Email}} and password {{.Password}} has been imported.\r\n"
@@ -2462,6 +2554,38 @@
"id": "api.templates.verify_subject",
"translation": "[{{ .SiteName }}] Email Verification"
},
+ {
+ "id": "api.templates.warn_metric_ack.body.contact_email_header",
+ "translation": "Email: "
+ },
+ {
+ "id": "api.templates.warn_metric_ack.body.contact_name_header",
+ "translation": "Contact: "
+ },
+ {
+ "id": "api.templates.warn_metric_ack.body.diagnostic_id_header",
+ "translation": "Diagnostic Id: "
+ },
+ {
+ "id": "api.templates.warn_metric_ack.body.registered_users_header",
+ "translation": "Total Active Users: "
+ },
+ {
+ "id": "api.templates.warn_metric_ack.body.site_url_header",
+ "translation": "Site URL: "
+ },
+ {
+ "id": "api.templates.warn_metric_ack.footer",
+ "translation": "If you have any additional inquiries, please contact support@mattermost.com"
+ },
+ {
+ "id": "api.templates.warn_metric_ack.number_of_active_users.body",
+ "translation": "Mattermost Contact Us request. My team has now {{ .Limit }} users and I am considering Mattermost Enterprise Edition."
+ },
+ {
+ "id": "api.templates.warn_metric_ack.subject",
+ "translation": "Mattermost Contact Us request"
+ },
{
"id": "api.templates.welcome_body.app_download_info",
"translation": "For the best experience, download the apps for PC, Mac, iOS and Android from:"
@@ -4202,6 +4326,26 @@
"id": "app.submit_interactive_dialog.json_error",
"translation": "Encountered an error encoding JSON for the interactive dialog."
},
+ {
+ "id": "app.system.warn_metric.bot_description",
+ "translation": "[Learn more about the Mattermost Advisor](https://about.mattermost.com/default-channel-handle-documentation)"
+ },
+ {
+ "id": "app.system.warn_metric.bot_displayname",
+ "translation": "Mattermost Advisor"
+ },
+ {
+ "id": "app.system.warn_metric.notification.empty_admin_list.app_error",
+ "translation": "List of admins is empty."
+ },
+ {
+ "id": "app.system.warn_metric.notification.invalid_metric.app_error",
+ "translation": "Could not find metric."
+ },
+ {
+ "id": "app.system.warn_metric.store.app_error",
+ "translation": "Failed to store value for {{.WarnMetricName}}"
+ },
{
"id": "app.system_install_date.parse_int.app_error",
"translation": "Failed to parse installation date."
diff --git a/model/bot.go b/model/bot.go
index 15ef6a70ca..fb46be495c 100644
--- a/model/bot.go
+++ b/model/bot.go
@@ -13,9 +13,10 @@ import (
)
const (
- BOT_DISPLAY_NAME_MAX_RUNES = USER_FIRST_NAME_MAX_RUNES
- BOT_DESCRIPTION_MAX_RUNES = 1024
- BOT_CREATOR_ID_MAX_RUNES = KEY_VALUE_PLUGIN_ID_MAX_RUNES // UserId or PluginId
+ BOT_DISPLAY_NAME_MAX_RUNES = USER_FIRST_NAME_MAX_RUNES
+ BOT_DESCRIPTION_MAX_RUNES = 1024
+ BOT_CREATOR_ID_MAX_RUNES = KEY_VALUE_PLUGIN_ID_MAX_RUNES // UserId or PluginId
+ BOT_WARN_METRIC_BOT_USERNAME = "mattermost-advisor"
)
// Bot is a special type of User meant for programmatic interactions.
diff --git a/model/config.go b/model/config.go
index 12419ab071..6c5776a9dc 100644
--- a/model/config.go
+++ b/model/config.go
@@ -47,6 +47,7 @@ const (
GENERIC_NO_CHANNEL_NOTIFICATION = "generic_no_channel"
GENERIC_NOTIFICATION = "generic"
GENERIC_NOTIFICATION_SERVER = "https://push-test.mattermost.com"
+ MM_SUPPORT_ADDRESS = "support@mattermost.com"
FULL_NOTIFICATION = "full"
ID_LOADED_NOTIFICATION = "id_loaded"
diff --git a/model/post.go b/model/post.go
index 852a504f9c..7c27eca9b6 100644
--- a/model/post.go
+++ b/model/post.go
@@ -64,6 +64,7 @@ const (
POST_PROPS_MENTION_HIGHLIGHT_DISABLED = "mentionHighlightDisabled"
POST_PROPS_GROUP_HIGHLIGHT_DISABLED = "disable_group_highlight"
+ POST_SYSTEM_WARN_METRIC_STATUS = "warn_metric_status"
)
var AT_MENTION_PATTEN = regexp.MustCompile(`\B@`)
@@ -312,7 +313,8 @@ func (o *Post) IsValid(maxPostSize int) *AppError {
POST_CHANNEL_RESTORED,
POST_CHANGE_CHANNEL_PRIVACY,
POST_ME,
- POST_ADD_BOT_TEAMS_CHANNELS:
+ POST_ADD_BOT_TEAMS_CHANNELS,
+ POST_SYSTEM_WARN_METRIC_STATUS:
default:
if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) {
return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)
diff --git a/model/system.go b/model/system.go
index b3100c92db..1ad0775c93 100644
--- a/model/system.go
+++ b/model/system.go
@@ -10,16 +10,26 @@ import (
)
const (
- SYSTEM_DIAGNOSTIC_ID = "DiagnosticId"
- SYSTEM_RAN_UNIT_TESTS = "RanUnitTests"
- SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime"
- SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId"
- SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime"
- SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey"
- SYSTEM_POST_ACTION_COOKIE_SECRET = "PostActionCookieSecret"
- SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate"
- SYSTEM_FIRST_SERVER_RUN_TIMESTAMP_KEY = "FirstServerRunTimestamp"
- SYSTEM_CLUSTER_ENCRYPTION_KEY = "ClusterEncryptionKey"
+ SYSTEM_DIAGNOSTIC_ID = "DiagnosticId"
+ SYSTEM_RAN_UNIT_TESTS = "RanUnitTests"
+ SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime"
+ SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId"
+ SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime"
+ SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey"
+ SYSTEM_POST_ACTION_COOKIE_SECRET = "PostActionCookieSecret"
+ SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate"
+ SYSTEM_FIRST_SERVER_RUN_TIMESTAMP_KEY = "FirstServerRunTimestamp"
+ SYSTEM_CLUSTER_ENCRYPTION_KEY = "ClusterEncryptionKey"
+ SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200 = "warn_metric_number_of_active_users_200"
+ SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400 = "warn_metric_number_of_active_users_400"
+ SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500 = "warn_metric_number_of_active_users_500"
+)
+
+const (
+ WARN_METRIC_STATUS_LIMIT_REACHED = "true"
+ WARN_METRIC_STATUS_RUNONCE = "runonce"
+ WARN_METRIC_STATUS_ACK = "ack"
+ WARN_METRIC_STATUS_STORE_PREFIX = "warn_metric_"
)
type System struct {
@@ -70,3 +80,78 @@ func ServerBusyStateFromJson(r io.Reader) *ServerBusyState {
json.NewDecoder(r).Decode(&sbs)
return sbs
}
+
+var WarnMetricsTable = map[string]WarnMetric{
+ SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200: {
+ Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200,
+ Limit: 200,
+ IsBotOnly: true,
+ IsRunOnce: true,
+ },
+ SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400: {
+ Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400,
+ Limit: 400,
+ IsBotOnly: true,
+ IsRunOnce: true,
+ },
+ SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500: {
+ Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500,
+ Limit: 500,
+ IsBotOnly: false,
+ IsRunOnce: false,
+ },
+}
+
+type WarnMetric struct {
+ Id string
+ Limit int64
+ IsBotOnly bool
+ IsRunOnce bool
+}
+
+type WarnMetricDisplayTexts struct {
+ BotTitle string
+ BotMessageBody string
+ BotMailToBody string
+ EmailBody string
+}
+type WarnMetricStatus struct {
+ Id string `json:"id"`
+ Limit int64 `json:"limit"`
+ Acked bool `json:"acked"`
+ StoreStatus string `json:"store_status,omitempty"`
+}
+
+func (wms *WarnMetricStatus) ToJson() string {
+ b, _ := json.Marshal(wms)
+ return string(b)
+}
+
+func WarnMetricStatusFromJson(data io.Reader) *WarnMetricStatus {
+ var o WarnMetricStatus
+ if err := json.NewDecoder(data).Decode(&o); err != nil {
+ return nil
+ } else {
+ return &o
+ }
+}
+
+func MapWarnMetricStatusToJson(o map[string]*WarnMetricStatus) string {
+ b, _ := json.Marshal(o)
+ return string(b)
+}
+
+type SendWarnMetricAck struct {
+ ForceAck bool `json:"forceAck"`
+}
+
+func (swma *SendWarnMetricAck) ToJson() string {
+ b, _ := json.Marshal(swma)
+ return string(b)
+}
+
+func SendWarnMetricAckFromJson(r io.Reader) *SendWarnMetricAck {
+ var swma *SendWarnMetricAck
+ json.NewDecoder(r).Decode(&swma)
+ return swma
+}
diff --git a/model/websocket_message.go b/model/websocket_message.go
index 0fd05ef3dd..281b50cff2 100644
--- a/model/websocket_message.go
+++ b/model/websocket_message.go
@@ -66,6 +66,8 @@ const (
WEBSOCKET_EVENT_SIDEBAR_CATEGORY_UPDATED = "sidebar_category_updated"
WEBSOCKET_EVENT_SIDEBAR_CATEGORY_DELETED = "sidebar_category_deleted"
WEBSOCKET_EVENT_SIDEBAR_CATEGORY_ORDER_UPDATED = "sidebar_category_order_updated"
+ WEBSOCKET_WARN_METRIC_STATUS_RECEIVED = "warn_metric_status_received"
+ WEBSOCKET_WARN_METRIC_STATUS_REMOVED = "warn_metric_status_removed"
)
type WebSocketMessage interface {
diff --git a/services/mailservice/mail.go b/services/mailservice/mail.go
index 3e392cb759..33fde79e7c 100644
--- a/services/mailservice/mail.go
+++ b/services/mailservice/mail.go
@@ -29,6 +29,7 @@ type mailData struct {
mimeTo string
smtpTo string
from mail.Address
+ cc string
replyTo mail.Address
subject string
htmlBody string
@@ -250,7 +251,7 @@ func TestConnection(config *model.Config) *model.AppError {
return nil
}
-func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *model.Config, enableComplianceFeatures bool) *model.AppError {
+func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *model.Config, enableComplianceFeatures bool, ccMail string) *model.AppError {
fromMail := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.FeedbackEmail}
replyTo := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.ReplyToAddress}
@@ -258,6 +259,7 @@ func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embedded
mimeTo: to,
smtpTo: to,
from: fromMail,
+ cc: ccMail,
replyTo: replyTo,
subject: subject,
htmlBody: htmlBody,
@@ -267,8 +269,8 @@ func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embedded
return sendMailUsingConfigAdvanced(mail, config, enableComplianceFeatures)
}
-func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool) *model.AppError {
- return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures)
+func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool, ccMail string) *model.AppError {
+ return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures, ccMail)
}
// allows for sending an email with attachments and differing MIME/SMTP recipients
@@ -328,6 +330,10 @@ func SendMail(c smtpClient, mail mailData, fileBackend filesstore.FileBackend, d
headers["Reply-To"] = []string{mail.replyTo.String()}
}
+ if len(mail.cc) > 0 {
+ headers["CC"] = []string{mail.cc}
+ }
+
for k, v := range mail.mimeHeaders {
headers[k] = []string{encodeRFC2047Word(v)}
}
diff --git a/services/mailservice/mail_test.go b/services/mailservice/mail_test.go
index 5bbe8c565e..cffe819a25 100644
--- a/services/mailservice/mail_test.go
+++ b/services/mailservice/mail_test.go
@@ -138,11 +138,12 @@ func TestSendMailUsingConfig(t *testing.T) {
var emailTo = "test@example.com"
var emailSubject = "Testing this email"
var emailBody = "This is a test from autobot"
+ var emailCC = "test@example.com"
//Delete all the messages before check the sample email
DeleteMailBox(emailTo)
- err2 := SendMailUsingConfig(emailTo, emailSubject, emailBody, cfg, true)
+ err2 := SendMailUsingConfig(emailTo, emailSubject, emailBody, cfg, true, emailCC)
require.Nil(t, err2, "Should connect to the SMTP Server")
//Check if the email was send to the right email address
@@ -176,6 +177,7 @@ func TestSendMailWithEmbeddedFilesUsingConfig(t *testing.T) {
var emailTo = "test@example.com"
var emailSubject = "Testing this email"
var emailBody = "This is a test from autobot"
+ var emailCC = "test@example.com"
//Delete all the messages before check the sample email
DeleteMailBox(emailTo)
@@ -184,7 +186,7 @@ func TestSendMailWithEmbeddedFilesUsingConfig(t *testing.T) {
"test1.png": bytes.NewReader([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")),
"test2.png": bytes.NewReader([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")),
}
- err2 := SendMailWithEmbeddedFilesUsingConfig(emailTo, emailSubject, emailBody, embeddedFiles, cfg, true)
+ err2 := SendMailWithEmbeddedFilesUsingConfig(emailTo, emailSubject, emailBody, embeddedFiles, cfg, true, emailCC)
require.Nil(t, err2, "Should connect to the SMTP Server")
//Check if the email was send to the right email address
@@ -416,7 +418,7 @@ func TestSendMail(t *testing.T) {
for testName, tc := range testCases {
t.Run(testName, func(t *testing.T) {
- mail := mailData{"", "", mail.Address{}, tc.replyTo, "", "", nil, nil, nil}
+ mail := mailData{"", "", mail.Address{}, "", tc.replyTo, "", "", nil, nil, nil}
appErr = SendMail(mocm, mail, mockBackend, time.Now())
require.Nil(t, appErr)
if len(tc.contains) > 0 {
diff --git a/templates/warn_metric_ack.html b/templates/warn_metric_ack.html
new file mode 100644
index 0000000000..0365b58612
--- /dev/null
+++ b/templates/warn_metric_ack.html
@@ -0,0 +1,84 @@
+{{define "warn_metric_ack"}}
+
+
+
+
+
+
+
+
+
+
+ {{.Props.Title}}
+ {{.Props.Info}} {{.Props.Warning}}
+ |
+
+
+
+
+
+
+ |
+
+ {{.Props.ContactNameHeader}}
+ {{.Props.ContactNameValue}}
+ |
+
+
+ {{if .Props.ContactEmailValue}}
+
+ |
+
+ {{.Props.ContactEmailHeader}}
+ {{.Props.ContactEmailValue}}
+ |
+
+ {{end}}
+ {{if .Props.RegisteredUsersValue}}
+
+ |
+
+ {{.Props.RegisteredUsersHeader}}
+ {{.Props.RegisteredUsersValue}}
+ |
+
+ {{end}}
+ {{if .Props.SiteURL}}
+
+ |
+ {{.Props.SiteURLHeader}}
+ {{.Props.SiteURL}}
+ |
+
+ {{end}}
+ {{if .Props.DiagnosticIdValue}}
+
+ |
+
+ {{.Props.DiagnosticIdHeader}}
+ {{.Props.DiagnosticIdValue}}
+ |
+
+ {{end}}
+
+ |
+
+
+ |
+
+
+ {{template "email_footer" . }}
+
+
+ |
+
+
+
+
+
+
+{{end}}
diff --git a/web/params.go b/web/params.go
index b9690a06fe..ad4f12234d 100644
--- a/web/params.go
+++ b/web/params.go
@@ -78,6 +78,7 @@ type Params struct {
FilterAllowReference bool
FilterParentTeamPermitted bool
CategoryId string
+ WarnMetricId string
}
func ParamsFromRequest(r *http.Request) *Params {
@@ -316,5 +317,9 @@ func ParamsFromRequest(r *http.Request) *Params {
params.IncludeDeleted = val
}
+ if val, ok := props["warn_metric_id"]; ok {
+ params.WarnMetricId = val
+ }
+
return params
}