From f557666b4d4396b4b4b7cf2bd068ec3a8958df6b Mon Sep 17 00:00:00 2001 From: Jakub Date: Thu, 17 Aug 2023 15:08:54 +0200 Subject: [PATCH] feat(GODT-2871): is telemetry enabled as service. --- go.mod | 2 +- go.sum | 4 +- internal/services/telemetry/service.go | 161 +++++++++++++++++++ internal/services/telemetry/service_test.go | 126 +++++++++++++++ internal/services/userevents/subscription.go | 26 ++- internal/user/user.go | 22 +-- 6 files changed, 321 insertions(+), 20 deletions(-) create mode 100644 internal/services/telemetry/service.go create mode 100644 internal/services/telemetry/service_test.go diff --git a/go.mod b/go.mod index a4e77f18..91eee06d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 github.com/ProtonMail/gluon v0.17.1-0.20230817061728-c2e6c5429251 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a - github.com/ProtonMail/go-proton-api v0.4.1-0.20230814133746-f04227131310 + github.com/ProtonMail/go-proton-api v0.4.1-0.20230821120427-4da287288372 github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton github.com/PuerkitoBio/goquery v1.8.1 github.com/abiosoft/ishell v2.0.0+incompatible diff --git a/go.sum b/go.sum index 17d94b8f..e0af3f30 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230814133746-f04227131310 h1:phvSTOUN0dIJmt/WSjpQcxcF0jG43+RBtlDpBAYLBKo= -github.com/ProtonMail/go-proton-api v0.4.1-0.20230814133746-f04227131310/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230821120427-4da287288372 h1:A5YuTZFJiqcr7axtlUqWEqKOmxyO97KJVZJzl9InwPA= +github.com/ProtonMail/go-proton-api v0.4.1-0.20230821120427-4da287288372/go.mod h1:nS8hMGjJLgC0Iej0JMYbsI388LesEkM1Hj/jCCxQeaQ= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= github.com/ProtonMail/gopenpgp/v2 v2.7.1-proton h1:YS6M20yvjCJPR1r4ADW5TPn6rahs4iAyZaACei86bEc= diff --git a/internal/services/telemetry/service.go b/internal/services/telemetry/service.go new file mode 100644 index 00000000..cc378718 --- /dev/null +++ b/internal/services/telemetry/service.go @@ -0,0 +1,161 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package telemetry + +import ( + "context" + "fmt" + + "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks" + "github.com/ProtonMail/proton-bridge/v3/internal/services/userevents" + "github.com/ProtonMail/proton-bridge/v3/pkg/cpc" + "github.com/sirupsen/logrus" +) + +type SettingsGetter interface { + GetUserSettings(ctx context.Context) (proton.UserSettings, error) +} + +type Service struct { + cpc *cpc.CPC + log *logrus.Entry + + eventService userevents.Subscribable + subscription *userevents.EventChanneledSubscriber + + userID string + settingsGetter SettingsGetter + + isTelemetryEnabled bool + isInitialised bool +} + +func NewService( + userID string, + settingsGetter SettingsGetter, + eventService userevents.Subscribable, +) *Service { + s := &Service{ + cpc: cpc.NewCPC(), + log: logrus.WithFields(logrus.Fields{ + "user": userID, + "service": "telemetry", + }), + + eventService: eventService, + subscription: userevents.NewEventSubscriber( + fmt.Sprintf("telemetry-%v", userID), + ), + + userID: userID, + settingsGetter: settingsGetter, + } + + s.initialise() + + return s +} + +func (s *Service) initialise() { + settings, err := s.settingsGetter.GetUserSettings(context.Background()) + if err != nil { + logrus.WithError(err).Error("Cannot get telemetry settings, asuming off") + s.isInitialised = false + s.isTelemetryEnabled = false + + return + } + + logrus.WithField("telemetry", settings.Telemetry).Debug("Telemetry initialised") + s.isInitialised = true + s.isTelemetryEnabled = settings.Telemetry == proton.SettingEnabled +} + +func (s *Service) Start(ctx context.Context, group *orderedtasks.OrderedCancelGroup) { + group.Go(ctx, s.userID, "telemetry-service", s.run) +} + +func (s *Service) run(ctx context.Context) { + s.log.Info("Starting service main loop") + defer s.log.Info("Exiting service main loop") + defer s.cpc.Close() + + eventHandler := userevents.EventHandler{ + RefreshHandler: s, + UserSettingsHandler: s, + } + + s.eventService.Subscribe(s.subscription) + defer s.eventService.Unsubscribe(s.subscription) + + for { + select { + case <-ctx.Done(): + return + + case request, ok := <-s.cpc.ReceiveCh(): + if !ok { + return + } + + switch request.Value().(type) { + case *isTelemetryEnabledReq: + s.log.Debug("Received is telemetry enabled request") + if !s.isInitialised { + s.initialise() + } + + request.Reply(ctx, s.isTelemetryEnabled, nil) + + default: + s.log.Error("Received unknown request") + } + case e, ok := <-s.subscription.OnEventCh(): + if !ok { + continue + } + e.Consume(func(event proton.Event) error { + return eventHandler.OnEvent(ctx, event) + }) + } + } +} + +func (s *Service) HandleRefreshEvent(_ context.Context, _ proton.RefreshFlag) error { + s.initialise() + return nil +} + +func (s *Service) HandleUserSettingsEvent(_ context.Context, settings *proton.UserSettings) error { + s.isTelemetryEnabled = settings.Telemetry == proton.SettingEnabled + s.isInitialised = true + return nil +} + +type isTelemetryEnabledReq struct{} + +func (s *Service) IsTelemetryEnabled(ctx context.Context) bool { + enabled, err := cpc.SendTyped[bool](ctx, s.cpc, &isTelemetryEnabledReq{}) + if err != nil { + s.log.WithError(err).Error("Failed to retrieve IsTelemeteryEnabled, assuming no") + return false + } + + return enabled +} diff --git a/internal/services/telemetry/service_test.go b/internal/services/telemetry/service_test.go new file mode 100644 index 00000000..5bcbcabc --- /dev/null +++ b/internal/services/telemetry/service_test.go @@ -0,0 +1,126 @@ +// Copyright (c) 2023 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package telemetry + +import ( + "context" + "errors" + "testing" + + "github.com/ProtonMail/gluon/async" + "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks" + "github.com/ProtonMail/proton-bridge/v3/internal/services/userevents" + "github.com/stretchr/testify/require" +) + +const TestUserID = "MyUserID" + +type mockSettingsGetter struct { + settings proton.UserSettings + err error +} + +func (m *mockSettingsGetter) GetUserSettings(_ context.Context) (proton.UserSettings, error) { + return m.settings, m.err +} + +func TestService_Unitialised(t *testing.T) { + mockSettingsGetter := &mockSettingsGetter{ + settings: proton.UserSettings{Telemetry: proton.SettingEnabled}, + err: errors.New("cannot get user settings"), + } + + service := NewService( + TestUserID, + mockSettingsGetter, + &userevents.NoOpSubscribable{}, + ) + + require.False(t, service.isInitialised) + require.False(t, service.isTelemetryEnabled) + + ctx := context.Background() + group := orderedtasks.NewOrderedCancelGroup(async.NoopPanicHandler{}) + defer group.CancelAndWait() + + service.Start(ctx, group) + require.False(t, service.IsTelemetryEnabled(ctx)) + require.False(t, service.isInitialised) + require.False(t, service.isTelemetryEnabled) + + mockSettingsGetter.err = nil + require.True(t, service.IsTelemetryEnabled(ctx)) + require.True(t, service.isInitialised) + require.True(t, service.isTelemetryEnabled) +} + +func TestService_OnUserSettingsEvent(t *testing.T) { + mockSettingsGetter := &mockSettingsGetter{} + + service := NewService( + TestUserID, + mockSettingsGetter, + &userevents.NoOpSubscribable{}, + ) + + require.True(t, service.isInitialised) + + ctx := context.Background() + group := orderedtasks.NewOrderedCancelGroup(async.NoopPanicHandler{}) + defer group.CancelAndWait() + + service.Start(ctx, group) + require.False(t, service.IsTelemetryEnabled(ctx)) + + require.NoError(t, service.HandleUserSettingsEvent(context.Background(), &proton.UserSettings{Telemetry: proton.SettingEnabled})) + require.True(t, service.IsTelemetryEnabled(ctx)) + + require.NoError(t, service.HandleUserSettingsEvent(context.Background(), &proton.UserSettings{Telemetry: proton.SettingDisabled})) + require.False(t, service.IsTelemetryEnabled(ctx)) +} + +func TestService_Unitialised_OnUserSettingsEvent(t *testing.T) { + mockSettingsGetter := &mockSettingsGetter{ + settings: proton.UserSettings{Telemetry: proton.SettingEnabled}, + err: errors.New("cannot get user settings"), + } + + service := NewService( + TestUserID, + mockSettingsGetter, + &userevents.NoOpSubscribable{}, + ) + + require.False(t, service.isInitialised) + require.False(t, service.isTelemetryEnabled) + + ctx := context.Background() + group := orderedtasks.NewOrderedCancelGroup(async.NoopPanicHandler{}) + defer group.CancelAndWait() + + service.Start(ctx, group) + require.False(t, service.IsTelemetryEnabled(ctx)) + require.False(t, service.isInitialised) + require.False(t, service.isTelemetryEnabled) + + require.NoError(t, service.HandleUserSettingsEvent(context.Background(), &proton.UserSettings{Telemetry: proton.SettingEnabled})) + require.True(t, service.IsTelemetryEnabled(ctx)) + require.True(t, service.isInitialised) + require.True(t, service.isTelemetryEnabled) +} diff --git a/internal/services/userevents/subscription.go b/internal/services/userevents/subscription.go index b5f4ef85..b25b7b7d 100644 --- a/internal/services/userevents/subscription.go +++ b/internal/services/userevents/subscription.go @@ -31,12 +31,13 @@ func NewEventSubscriber(name string) *EventChanneledSubscriber { } type EventHandler struct { - RefreshHandler RefreshEventHandler - AddressHandler AddressEventHandler - UserHandler UserEventHandler - LabelHandler LabelEventHandler - MessageHandler MessageEventHandler - UsedSpaceHandler UserUsedSpaceEventHandler + RefreshHandler RefreshEventHandler + AddressHandler AddressEventHandler + UserHandler UserEventHandler + LabelHandler LabelEventHandler + MessageHandler MessageEventHandler + UsedSpaceHandler UserUsedSpaceEventHandler + UserSettingsHandler UserSettingsHandler } func (e EventHandler) OnEvent(ctx context.Context, event proton.Event) error { @@ -44,7 +45,14 @@ func (e EventHandler) OnEvent(ctx context.Context, event proton.Event) error { return e.RefreshHandler.HandleRefreshEvent(ctx, event.Refresh) } - // Start with user events. + // Start with user settings because of telemetry. + if event.UserSettings != nil { + if err := e.UserSettingsHandler.HandleUserSettingsEvent(ctx, event.UserSettings); err != nil { + return fmt.Errorf("failed to apply user event: %w", err) + } + } + + // Continue with user events. if event.User != nil && e.UserHandler != nil { if err := e.UserHandler.HandleUserEvent(ctx, event.User); err != nil { return fmt.Errorf("failed to apply user event: %w", err) @@ -93,6 +101,10 @@ type UserUsedSpaceEventHandler interface { HandleUsedSpaceEvent(ctx context.Context, newSpace int) error } +type UserSettingsHandler interface { + HandleUserSettingsEvent(ctx context.Context, settings *proton.UserSettings) error +} + type AddressEventHandler interface { HandleAddressEvents(ctx context.Context, events []proton.AddressEvent) error } diff --git a/internal/user/user.go b/internal/user/user.go index 40b6d232..5274e2e5 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -34,6 +34,7 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks" "github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder" "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp" + telemetryservice "github.com/ProtonMail/proton-bridge/v3/internal/services/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/services/userevents" "github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry" @@ -78,10 +79,11 @@ type User struct { // goStatusProgress triggers a check/sending if progress is needed. goStatusProgress func() - eventService *userevents.Service - identityService *useridentity.Service - smtpService *smtp.Service - imapService *imapservice.Service + eventService *userevents.Service + identityService *useridentity.Service + smtpService *smtp.Service + imapService *imapservice.Service + telemetryService *telemetryservice.Service serviceGroup *orderedtasks.OrderedCancelGroup } @@ -215,6 +217,8 @@ func newImpl( user.identityService = useridentity.NewService(user.eventService, user, identityState, encVault, user) + user.telemetryService = telemetryservice.NewService(apiUser.ID, client, user.eventService) + user.smtpService = smtp.NewService( apiUser.ID, client, @@ -283,6 +287,9 @@ func newImpl( return user, fmt.Errorf("failed to start event service: %w", err) } + // Start Telemetry Service + user.telemetryService.Start(ctx, user.serviceGroup) + // Start Identity Service user.identityService.Start(ctx, user.serviceGroup) @@ -627,12 +634,7 @@ func (user *User) Close() { // IsTelemetryEnabled check if the telemetry is enabled or disabled for this user. func (user *User) IsTelemetryEnabled(ctx context.Context) bool { - settings, err := user.client.GetUserSettings(ctx) - if err != nil { - user.log.WithError(err).Error("Failed to retrieve API user Settings") - return false - } - return settings.Telemetry == proton.SettingEnabled + return user.telemetryService.IsTelemetryEnabled(ctx) } // SendTelemetry send telemetry request.