diff --git a/go.mod b/go.mod index eac3ae610..1c435cd63 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/mattermost/calls-recorder v0.4.2 github.com/mattermost/logr/v2 v2.0.16 github.com/mattermost/mattermost-plugin-calls/server/public v0.0.1 - github.com/mattermost/mattermost/server/public v0.0.9-0.20230824163353-69c11cfe1403 + github.com/mattermost/mattermost/server/public v0.0.10-0.20231109142142-8ffda2b73ea8 github.com/mattermost/rtcd v0.12.0 github.com/mattermost/squirrel v0.2.0 github.com/pion/interceptor v0.1.25 @@ -47,7 +47,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.4.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.4 // indirect - github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-plugin v1.4.10 // indirect github.com/hashicorp/yamux v0.1.1 // indirect diff --git a/go.sum b/go.sum index 2b34e2209..ba3237293 100644 --- a/go.sum +++ b/go.sum @@ -125,9 +125,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -184,7 +181,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -221,8 +217,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/pyroscope-go/godeltaprof v0.1.4 h1:mDsJ3ngul7UfrHibGQpV66PbZ3q1T8glz/tK3bQKKEk= github.com/grafana/pyroscope-go/godeltaprof v0.1.4/go.mod h1:1HSPtjU8vLG0jE9JrTdzjgFqdJ/VgN7fvxBNq3luJko= -github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a h1:i0+Se9S+2zL5CBxJouqn2Ej6UQMwH1c57ZB6DVnqck4= -github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -301,8 +295,8 @@ github.com/mattermost/ldap v3.0.4+incompatible h1:SOeNnz+JNR+foQ3yHkYqijb9MLPhXN github.com/mattermost/ldap v3.0.4+incompatible/go.mod h1:b4reDCcGpBxJ4WX0f224KFY+OR0npin7or7EFpeIko4= github.com/mattermost/logr/v2 v2.0.16 h1:jnePX4cPskC3WDFvUardh/xZfxNdsFXbEERJQ1kUEDE= github.com/mattermost/logr/v2 v2.0.16/go.mod h1:1dm/YhTpozsqANXxo5Pi5zYLBsal2xY0pX+JZNbzYJY= -github.com/mattermost/mattermost/server/public v0.0.9-0.20230824163353-69c11cfe1403 h1:/rxsEaisu+Rb5mWfoIEnbFqscJeKVkspj+BWzchUAfs= -github.com/mattermost/mattermost/server/public v0.0.9-0.20230824163353-69c11cfe1403/go.mod h1:sgXQrYzs+IJy51mB8E8OBljagk2u3YwQRoYlBH5goiw= +github.com/mattermost/mattermost/server/public v0.0.10-0.20231109142142-8ffda2b73ea8 h1:tZhcvX1k526YLHeO8lQpFa8J2Ejr7sHEYdzst5xrIjA= +github.com/mattermost/mattermost/server/public v0.0.10-0.20231109142142-8ffda2b73ea8/go.mod h1:AMuKlAabVFnLvf5bXg+69EdvtLIfty4ahFossB34LM4= github.com/mattermost/rtcd v0.12.0 h1:T/07JYExpwMmWq58QIp8KU08FWOzVmmatzLaEOIMP4A= github.com/mattermost/rtcd v0.12.0/go.mod h1:s0J7u93n2e2YPATEWCiiJ19iQf2TTHJapXQR2POE8KA= github.com/mattermost/squirrel v0.2.0 h1:8ZWeyf+MWQ2cL7hu9REZgLtz2IJi51qqZEovI3T3TT8= @@ -353,7 +347,6 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= @@ -570,8 +563,6 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= -go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= diff --git a/server/activate.go b/server/activate.go index 3367e86d4..35c573481 100644 --- a/server/activate.go +++ b/server/activate.go @@ -124,6 +124,8 @@ func (p *Plugin) OnActivate() error { }() } + p.loadPushProxyVersion() + if rtcdURL := cfg.getRTCDURL(); rtcdURL != "" && p.licenseChecker.RTCDAllowed() { rtcdManager, err := p.newRTCDClientManager(rtcdURL) if err != nil { diff --git a/server/log.go b/server/log.go index fc5edee96..aca2680e1 100644 --- a/server/log.go +++ b/server/log.go @@ -143,3 +143,7 @@ func (l *logger) IsLevelEnabled(_ logr.Level) bool { func (l *logger) With(_ ...logr.Field) *mlog.Logger { return nil } + +func (l *logger) Sugar(_ ...mlog.Field) mlog.Sugar { + return mlog.Sugar{} +} diff --git a/server/plugin.go b/server/plugin.go index 7881157a3..fe1b29235 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -67,6 +67,9 @@ type Plugin struct { // Database handle to the writer DB node wDB *sql.DB driverName string + + // Push-proxy's version, refreshed when the plugin activates + pushProxyVersion string } func (p *Plugin) startSession(us *session, senderID string) { diff --git a/server/push_notifications.go b/server/push_notifications.go index 5cfa2d013..47a308f5a 100644 --- a/server/push_notifications.go +++ b/server/push_notifications.go @@ -5,6 +5,8 @@ import ( "github.com/mattermost/mattermost/server/public/model" ) +const minPushProxyVersionWithCalls = "5.27.0" + func (p *Plugin) NotificationWillBePushed(notification *model.PushNotification, userID string) (*model.PushNotification, string) { // We will use our own notifications if: // 1. This is a call start post @@ -38,6 +40,10 @@ func (p *Plugin) NotificationWillBePushed(notification *model.PushNotification, } func (p *Plugin) sendPushNotifications(channelID, createdPostID, threadID string, sender *model.User, config *model.Config) { + if err := p.canSendPushNotifications(config, p.API.GetLicense()); err != nil { + return + } + channel, appErr := p.API.GetChannel(channelID) if appErr != nil { p.LogError("failed to get channel", "error", appErr.Error()) @@ -54,6 +60,11 @@ func (p *Plugin) sendPushNotifications(channelID, createdPostID, threadID string return } + pushType := model.PushTypeMessage + if err := checkMinVersion(minPushProxyVersionWithCalls, p.pushProxyVersion); err == nil { + pushType = model.PushTypeCalls + } + for _, member := range members { if member.Id == sender.Id { continue @@ -61,7 +72,7 @@ func (p *Plugin) sendPushNotifications(channelID, createdPostID, threadID string msg := &model.PushNotification{ Version: model.PushMessageV2, - Type: model.PushTypeMessage, + Type: pushType, TeamId: channel.TeamId, ChannelId: channelID, PostId: createdPostID, @@ -71,7 +82,9 @@ func (p *Plugin) sendPushNotifications(channelID, createdPostID, threadID string Message: buildGenericPushNotificationMessage(), } - // This is ugly. + // This is ugly because it's a little complicated. We need to special case IdLoaded notifications (don't expose + // any details of the push notification on the wire). Otherwise, we can send more information, unless the server + // has set GenericNoChannel. if *config.EmailSettings.PushNotificationContents == model.IdLoadedNotification { msg.IsIdLoaded = p.checkLicenseForIDLoaded() } else { diff --git a/server/utils.go b/server/utils.go index 2be78dfe7..65b387fc7 100644 --- a/server/utils.go +++ b/server/utils.go @@ -6,6 +6,8 @@ package main import ( "bytes" "compress/zlib" + "encoding/json" + "errors" "fmt" "github.com/mattermost/mattermost/server/public/model" "io" @@ -63,6 +65,76 @@ func (p *Plugin) getNotificationNameFormat(userID string) string { return *config.TeamSettings.TeammateNameDisplay } +func (p *Plugin) loadPushProxyVersion() { + config := p.API.GetConfig() + if err := p.canSendPushNotifications(config, p.API.GetLicense()); err != nil { + p.LogWarn(err.Error()) + return + } + + client := NewClient() + var err error + p.pushProxyVersion, err = getPushProxyVersion(client, config) + if err != nil { + p.LogError(err.Error()) + } +} + +// getPushProxyVersion will return the version if the push proxy is reachable and version >= 5.27.0 +// which is when the "/version" endpoint was added. Otherwise it will return "" for lower versions and for +// failed attempts to get the version (which could mean only that the push proxy was unavailable temporarily) +func getPushProxyVersion(client *http.Client, config *model.Config) (string, error) { + if config == nil || + config.EmailSettings.PushNotificationServer == nil || + *config.EmailSettings.PushNotificationServer == "" { + return "", nil + } + serverURL := strings.TrimRight(*config.EmailSettings.PushNotificationServer, "/") + "/version" + req, err := http.NewRequest("GET", serverURL, nil) + if err != nil { + return "", fmt.Errorf("failed to build request, err: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("http request failed, err: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + var respData = struct { + Version string + Hash string + }{} + if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil { + return "", fmt.Errorf("failed to decode http response, err: %w", err) + } + + return respData.Version, nil + } + + // Must not be newer version of push proxy + return "", nil +} + +func (p *Plugin) canSendPushNotifications(config *model.Config, license *model.License) error { + if config == nil || + config.EmailSettings.SendPushNotifications == nil || + !*config.EmailSettings.SendPushNotifications { + return nil + } + + if config.EmailSettings.PushNotificationServer == nil { + return nil + } + pushServer := *config.EmailSettings.PushNotificationServer + if pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) { + return errors.New("push notifications have been disabled. Update your license or go to System Console > Environment > Push Notification Server to use a different server") + } + + return nil +} + func getChannelNameForNotification(channel *model.Channel, sender *model.User, users []*model.User, nameFormat, excludeID string) string { switch channel.Type { case model.ChannelTypeDirect: @@ -176,3 +248,11 @@ func mapKeys[K comparable, V any](m map[K]V) []K { } return keys } + +// NewClient creates a SimpleClient intended for one-off requests, like getPushProxyVersion. +// If we end up needing something more long term, we should consider storing it. +func NewClient() *http.Client { + return &http.Client{ + Timeout: 10 * time.Second, + } +} diff --git a/server/utils_test.go b/server/utils_test.go index c2d71178d..b8fce13c7 100644 --- a/server/utils_test.go +++ b/server/utils_test.go @@ -1,6 +1,8 @@ package main import ( + "errors" + "github.com/mattermost/mattermost/server/public/model" "net/http" "net/http/httptest" "testing" @@ -145,3 +147,167 @@ func TestCheckMinVersion(t *testing.T) { }) } } + +func TestPlugin_canSendPushNotifications(t *testing.T) { + config := &model.Config{ + EmailSettings: model.EmailSettings{ + SendPushNotifications: model.NewBool(true), + PushNotificationServer: model.NewString(model.MHPNS), + }, + } + license := &model.License{ + Features: &model.Features{ + MHPNS: model.NewBool(true), + }, + } + tests := []struct { + name string + config *model.Config + license *model.License + want error + }{ + { + name: "no config", + config: nil, + license: nil, + want: nil, + }, + { + name: "no push notification server", + config: &model.Config{ + EmailSettings: model.EmailSettings{ + SendPushNotifications: model.NewBool(true), + PushNotificationServer: nil, + }}, + license: nil, + want: nil, + }, + { + name: "push notification server blank", + config: &model.Config{ + EmailSettings: model.EmailSettings{ + SendPushNotifications: model.NewBool(true), + PushNotificationServer: model.NewString(""), + }}, + license: nil, + want: nil, + }, + { + name: "push notifications set to false", + config: &model.Config{ + EmailSettings: model.EmailSettings{ + SendPushNotifications: model.NewBool(false), + PushNotificationServer: model.NewString(model.MHPNS), + }}, + license: nil, + want: nil, + }, + { + name: "no license", + config: config, + license: nil, + want: errors.New("push notifications have been disabled. Update your license or go to System Console > Environment > Push Notification Server to use a different server"), + }, + { + name: "no MHPNS in license", + config: config, + license: &model.License{ + Features: &model.Features{ + MHPNS: model.NewBool(false), + }, + }, + want: errors.New("push notifications have been disabled. Update your license or go to System Console > Environment > Push Notification Server to use a different server"), + }, + { + name: "allowed", + config: config, + license: license, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Plugin{} + assert.Equalf(t, tt.want, p.canSendPushNotifications(tt.config, tt.license), "test: %s", tt.name) + }) + } +} + +func TestPlugin_getPushProxyVersion(t *testing.T) { + config := &model.Config{ + EmailSettings: model.EmailSettings{ + PushNotificationServer: model.NewString(model.MHPNS), + }, + } + + tests := []struct { + name string + config *model.Config + want string + handler func(http.ResponseWriter) + }{ + { + name: "config nil", + config: nil, + want: "", + }, + { + name: "no push server", + config: &model.Config{ + EmailSettings: model.EmailSettings{ + PushNotificationServer: model.NewString(""), + }, + }, + want: "", + }, + { + name: "nil push server", + config: &model.Config{ + EmailSettings: model.EmailSettings{ + PushNotificationServer: nil, + }, + }, + want: "", + }, + { + name: "404 (old push proxy)", + config: config, + want: "", + handler: func(w http.ResponseWriter) { + w.WriteHeader(http.StatusNotFound) + }, + }, + { + name: "got a version", + config: config, + want: "5.37.0", + handler: func(w http.ResponseWriter) { + _, _ = w.Write([]byte(`{"version": "5.37.0", "hash": "abcde3445"}`)) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + assert.Equal(t, "/version", req.URL.String()) + if tt.handler != nil { + tt.handler(w) + } + })) + defer server.Close() + + // Do we have a push server url? if so, rewrite it for the test. + // Why are we doing it this way? We want to test the variety of different states the config + // can be in, as well as the responses the push proxy can give us. + if tt.config != nil && + tt.config.EmailSettings.PushNotificationServer != nil && + *tt.config.EmailSettings.PushNotificationServer != "" { + tt.config.EmailSettings.PushNotificationServer = model.NewString(server.URL) + } + ret, err := getPushProxyVersion(server.Client(), tt.config) + + assert.NoError(t, err) + assert.Equalf(t, tt.want, ret, "test name: %s", tt.name) + }) + } +} diff --git a/webapp/src/utils.test.ts b/webapp/src/utils.test.ts index fbfe1eafb..cdc798aa2 100644 --- a/webapp/src/utils.test.ts +++ b/webapp/src/utils.test.ts @@ -206,7 +206,7 @@ describe('utils', () => { const sleepTimeMs = 500; const start = Date.now(); await sleep(sleepTimeMs); - expect(Date.now() - start).toBeGreaterThan(sleepTimeMs); + expect(Date.now() - start).toBeGreaterThanOrEqual(sleepTimeMs); }); });