-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Patches for v2 #716
Patches for v2 #716
Changes from 11 commits
0c2dd31
579651a
a4a26b2
214943c
69afc74
eb7d1cf
d5c2e1f
ad0a781
635aeb6
1c9902b
210af51
a77ab2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,8 +86,8 @@ func NewAPI(p *Plugin, store store.Store) *API { | |
|
||
// returnJSON writes the given data as json with the provided httpStatus | ||
func (a *API) returnJSON(w http.ResponseWriter, data any) { | ||
w.WriteHeader(http.StatusOK) | ||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusOK) | ||
|
||
if err := json.NewEncoder(w).Encode(data); err != nil { | ||
a.p.API.LogWarn("Failed to write to http.ResponseWriter", "error", err.Error()) | ||
|
@@ -879,31 +879,23 @@ func (a *API) siteStats(w http.ResponseWriter, r *http.Request) { | |
http.Error(w, "unable to get whitelisted users count", http.StatusInternalServerError) | ||
return | ||
} | ||
receiving, err := a.p.store.GetActiveUsersReceivingCount(metricsActiveUsersRange) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just simplifying. |
||
totalActiveUsers, err := a.p.store.GetActiveUsersCount(metricsActiveUsersRange) | ||
if err != nil { | ||
a.p.API.LogWarn("Failed to get users receiving count", "error", err.Error()) | ||
http.Error(w, "unable to get users receiving count", http.StatusInternalServerError) | ||
return | ||
} | ||
sending, err := a.p.store.GetActiveUsersSendingCount(metricsActiveUsersRange) | ||
if err != nil { | ||
a.p.API.LogWarn("Failed to get sending users count", "error", err.Error()) | ||
http.Error(w, "unable to get sending users count", http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
siteStats := struct { | ||
TotalConnectedUsers int64 `json:"total_connected_users"` | ||
PendingInvitedUsers int64 `json:"pending_invited_users"` | ||
CurrentWhitelistUsers int64 `json:"current_whitelist_users"` | ||
UserReceivingMessages int64 `json:"total_users_receiving"` | ||
UserSendingMessages int64 `json:"total_users_sending"` | ||
TotalActiveUsers int64 `json:"total_active_users"` | ||
}{ | ||
TotalConnectedUsers: connectedUsersCount, | ||
PendingInvitedUsers: int64(pendingInvites), | ||
CurrentWhitelistUsers: int64(whitelistedUsers), | ||
UserReceivingMessages: receiving, | ||
UserSendingMessages: sending, | ||
TotalActiveUsers: totalActiveUsers, | ||
} | ||
|
||
a.returnJSON(w, siteStats) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -154,7 +154,6 @@ func TestProcessActivity(t *testing.T) { | |
func TestProcessLifecycle(t *testing.T) { | ||
th := setupTestHelper(t) | ||
apiURL := th.pluginURL(t, "lifecycle") | ||
team := th.SetupTeam(t) | ||
|
||
sendRequest := func(t *testing.T, activities []msteams.Activity) (*http.Response, string) { | ||
t.Helper() | ||
|
@@ -222,7 +221,7 @@ func TestProcessLifecycle(t *testing.T) { | |
Resource: "mockResource", | ||
ChangeType: "mockChangeType", | ||
ClientState: "mockClientState", | ||
LifecycleEvent: "mockLifecycleEvent", | ||
LifecycleEvent: "reauthorizationRequired", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use real events, since we now check and discard unrecognized ones. |
||
}, | ||
} | ||
|
||
|
@@ -231,31 +230,48 @@ func TestProcessLifecycle(t *testing.T) { | |
assert.Equal(t, "Invalid webhook secret\n", bodyString) | ||
}) | ||
|
||
t.Run("valid event, no refresh needed", func(t *testing.T) { | ||
t.Run("valid payload, unknown subscription", func(t *testing.T) { | ||
th.Reset(t) | ||
|
||
channel := th.SetupPublicChannel(t, team) | ||
activities := []msteams.Activity{ | ||
{ | ||
SubscriptionID: model.NewId(), | ||
Resource: "mockResource", | ||
ClientState: "webhooksecret", | ||
ChangeType: "mockChangeType", | ||
LifecycleEvent: "reauthorizationRequired", | ||
}, | ||
} | ||
|
||
subscription := storemodels.ChannelSubscription{ | ||
response, bodyString := sendRequest(t, activities) | ||
assert.Equal(t, http.StatusOK, response.StatusCode) | ||
assert.Empty(t, bodyString) | ||
|
||
assert.Eventually(t, func() bool { | ||
return th.getRelativeCounter(t, | ||
"msteams_connect_events_lifecycle_events_total", | ||
withLabel("event_type", "reauthorizationRequired"), | ||
withLabel("discarded_reason", metrics.DiscardedReasonUnusedSubscription), | ||
) == 1 | ||
}, 5*time.Second, 500*time.Millisecond) | ||
assert.Never(t, func() bool { | ||
return th.getRelativeCounter(t, | ||
"msteams_connect_events_subscriptions_total", | ||
withLabel("action", metrics.SubscriptionRefreshed), | ||
) > 0 | ||
}, 5*time.Second, 500*time.Millisecond) | ||
}) | ||
|
||
t.Run("valid payload, unknown event", func(t *testing.T) { | ||
th.Reset(t) | ||
|
||
subscription := storemodels.GlobalSubscription{ | ||
SubscriptionID: model.NewId(), | ||
TeamID: model.NewId(), | ||
ChannelID: model.NewId(), | ||
Type: "allChats", | ||
ExpiresOn: time.Now().Add(10 * time.Minute), | ||
Secret: th.p.getConfiguration().WebhookSecret, | ||
} | ||
err := th.p.GetStore().SaveChannelSubscription(subscription) | ||
require.NoError(t, err) | ||
|
||
link := &storemodels.ChannelLink{ | ||
MattermostTeamID: channel.TeamId, | ||
MattermostTeamName: team.Name, | ||
MattermostChannelID: channel.Id, | ||
MattermostChannelName: channel.Name, | ||
MSTeamsTeam: subscription.TeamID, | ||
MSTeamsChannel: subscription.ChannelID, | ||
Creator: "creator_id", | ||
} | ||
err = th.p.GetStore().StoreChannelLink(link) | ||
err := th.p.GetStore().SaveGlobalSubscription(subscription) | ||
require.NoError(t, err) | ||
|
||
activities := []msteams.Activity{ | ||
|
@@ -264,7 +280,7 @@ func TestProcessLifecycle(t *testing.T) { | |
Resource: "mockResource", | ||
ClientState: "webhooksecret", | ||
ChangeType: "mockChangeType", | ||
LifecycleEvent: "mockLifecycleEvent", | ||
LifecycleEvent: "unknownEvent", | ||
}, | ||
} | ||
|
||
|
@@ -275,37 +291,28 @@ func TestProcessLifecycle(t *testing.T) { | |
assert.Eventually(t, func() bool { | ||
return th.getRelativeCounter(t, | ||
"msteams_connect_events_lifecycle_events_total", | ||
withLabel("event_type", "mockLifecycleEvent"), | ||
withLabel("discarded_reason", metrics.DiscardedReasonNone), | ||
withLabel("event_type", "unknownEvent"), | ||
withLabel("discarded_reason", metrics.DiscardedReasonUnknownLifecycleEvent), | ||
) == 1 | ||
}, 5*time.Second, 500*time.Millisecond) | ||
assert.Never(t, func() bool { | ||
return th.getRelativeCounter(t, | ||
"msteams_connect_events_subscriptions_total", | ||
withLabel("action", metrics.SubscriptionRefreshed), | ||
) > 0 | ||
}, 5*time.Second, 500*time.Millisecond) | ||
}) | ||
|
||
t.Run("valid event, refresh needed", func(t *testing.T) { | ||
th.Reset(t) | ||
|
||
channel := th.SetupPublicChannel(t, team) | ||
|
||
subscription := storemodels.ChannelSubscription{ | ||
subscription := storemodels.GlobalSubscription{ | ||
SubscriptionID: model.NewId(), | ||
TeamID: model.NewId(), | ||
ChannelID: model.NewId(), | ||
Type: "allChats", | ||
ExpiresOn: time.Now().Add(10 * time.Minute), | ||
Secret: th.p.getConfiguration().WebhookSecret, | ||
} | ||
err := th.p.GetStore().SaveChannelSubscription(subscription) | ||
require.NoError(t, err) | ||
|
||
link := &storemodels.ChannelLink{ | ||
MattermostTeamID: channel.TeamId, | ||
MattermostTeamName: team.Name, | ||
MattermostChannelID: channel.Id, | ||
MattermostChannelName: channel.Name, | ||
MSTeamsTeam: subscription.TeamID, | ||
MSTeamsChannel: subscription.ChannelID, | ||
Creator: "creator_id", | ||
} | ||
err = th.p.GetStore().StoreChannelLink(link) | ||
err := th.p.GetStore().SaveGlobalSubscription(subscription) | ||
require.NoError(t, err) | ||
|
||
activities := []msteams.Activity{ | ||
|
@@ -332,6 +339,12 @@ func TestProcessLifecycle(t *testing.T) { | |
withLabel("discarded_reason", metrics.DiscardedReasonNone), | ||
) == 1 | ||
}, 5*time.Second, 500*time.Millisecond) | ||
assert.Eventually(t, func() bool { | ||
return th.getRelativeCounter(t, | ||
"msteams_connect_events_subscriptions_total", | ||
withLabel("action", metrics.SubscriptionRefreshed), | ||
) == 1 | ||
}, 5*time.Second, 500*time.Millisecond) | ||
}) | ||
} | ||
|
||
|
@@ -935,7 +948,7 @@ func TestGetSiteStats(t *testing.T) { | |
|
||
response, bodyString := sendRequest(t, sysadmin) | ||
assert.Equal(t, http.StatusOK, response.StatusCode) | ||
assert.JSONEq(t, `{"current_whitelist_users":0, "pending_invited_users":0, "total_connected_users":0, "total_users_receiving":0, "total_users_sending":0}`, bodyString) | ||
assert.JSONEq(t, `{"current_whitelist_users":0, "pending_invited_users":0, "total_connected_users":0, "total_active_users":0}`, bodyString) | ||
}) | ||
|
||
t.Run("1 connected user", func(t *testing.T) { | ||
|
@@ -945,14 +958,12 @@ func TestGetSiteStats(t *testing.T) { | |
user1 := th.SetupUser(t, team) | ||
th.ConnectUser(t, user1.Id) | ||
|
||
err := th.p.store.SetUserLastChatSentAt(user1.Id, time.Now().Add(-3*24*time.Hour).UnixMicro()) | ||
require.NoError(t, err) | ||
err = th.p.store.SetUserLastChatReceivedAt(user1.Id, time.Now().Add(-4*24*time.Hour).UnixMicro()) | ||
err := th.p.store.SetUserLastChatReceivedAt(user1.Id, time.Now().Add(-4*24*time.Hour).UnixMicro()) | ||
require.NoError(t, err) | ||
|
||
response, bodyString := sendRequest(t, sysadmin) | ||
assert.Equal(t, http.StatusOK, response.StatusCode) | ||
assert.JSONEq(t, `{"current_whitelist_users":0, "pending_invited_users":0, "total_connected_users":1,"total_users_receiving":1, "total_users_sending":1}`, bodyString) | ||
assert.JSONEq(t, `{"current_whitelist_users":0, "pending_invited_users":0, "total_connected_users":1,"total_active_users":1}`, bodyString) | ||
}) | ||
|
||
t.Run("1 invited user, 2 whitelisted users", func(t *testing.T) { | ||
|
@@ -969,7 +980,7 @@ func TestGetSiteStats(t *testing.T) { | |
|
||
response, bodyString := sendRequest(t, sysadmin) | ||
assert.Equal(t, http.StatusOK, response.StatusCode) | ||
assert.JSONEq(t, `{"current_whitelist_users":2, "pending_invited_users":1, "total_connected_users":0,"total_users_receiving":0, "total_users_sending":0}`, bodyString) | ||
assert.JSONEq(t, `{"current_whitelist_users":2, "pending_invited_users":1, "total_connected_users":0,"total_active_users":0}`, bodyString) | ||
}) | ||
|
||
t.Run("10 connected users", func(t *testing.T) { | ||
|
@@ -987,18 +998,11 @@ func TestGetSiteStats(t *testing.T) { | |
err := th.p.store.SetUserLastChatReceivedAt(user.Id, time.Now().Add(-8*24*time.Hour).UnixMicro()) | ||
require.NoError(t, err) | ||
} | ||
if i < 2 { | ||
err := th.p.store.SetUserLastChatSentAt(user.Id, time.Now().Add(-3*24*time.Hour).UnixMicro()) | ||
require.NoError(t, err) | ||
} else { | ||
err := th.p.store.SetUserLastChatSentAt(user.Id, time.Now().Add(-8*24*time.Hour).UnixMicro()) | ||
require.NoError(t, err) | ||
} | ||
} | ||
|
||
response, bodyString := sendRequest(t, sysadmin) | ||
assert.Equal(t, http.StatusOK, response.StatusCode) | ||
assert.JSONEq(t, `{"current_whitelist_users":0, "pending_invited_users":0, "total_connected_users":10,"total_users_receiving":5, "total_users_sending":2}`, bodyString) | ||
assert.JSONEq(t, `{"current_whitelist_users":0, "pending_invited_users":0, "total_connected_users":10,"total_active_users":5}`, bodyString) | ||
}) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,12 @@ func (c *configuration) ProcessConfiguration() { | |
c.ClientSecret = strings.TrimSpace(c.ClientSecret) | ||
c.EncryptionKey = strings.TrimSpace(c.EncryptionKey) | ||
c.WebhookSecret = strings.TrimSpace(c.WebhookSecret) | ||
if c.MaxSizeForCompleteDownload < 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a bug with the system console where values can sometimes not get saved, making it brutal to keep losing the various secrets. If this happens for these values, just use sensible values. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So what are we actually fixing here, some default values that were missing? Anyone looking at the bug you mention? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm trying to work around https://community.mattermost.com/core/pl/o78pcxfj87f4frt3qhrqxw1x1y, specifically. |
||
c.MaxSizeForCompleteDownload = 0 | ||
} | ||
if c.BufferSizeForFileStreaming <= 0 { | ||
c.BufferSizeForFileStreaming = 20 | ||
} | ||
} | ||
|
||
func (p *Plugin) validateConfiguration(configuration *configuration) error { | ||
|
@@ -59,12 +65,6 @@ func (p *Plugin) validateConfiguration(configuration *configuration) error { | |
if configuration.WebhookSecret == "" { | ||
return errors.New("webhook secret should not be empty") | ||
} | ||
if configuration.MaxSizeForCompleteDownload < 0 { | ||
return errors.New("max size for complete single download should not be negative") | ||
} | ||
if configuration.BufferSizeForFileStreaming <= 0 { | ||
return errors.New("buffer size for file streaming should be greater than zero") | ||
} | ||
|
||
return nil | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package main | ||
|
||
import ( | ||
"database/sql" | ||
"errors" | ||
"regexp" | ||
"strings" | ||
|
@@ -141,18 +142,33 @@ func (ah *ActivityHandler) Handle(activity msteams.Activity) error { | |
} | ||
|
||
func (ah *ActivityHandler) HandleLifecycleEvent(event msteams.Activity) { | ||
if event.LifecycleEvent == "reauthorizationRequired" { | ||
expiresOn, err := ah.plugin.GetClientForApp().RefreshSubscription(event.SubscriptionID) | ||
if err != nil { | ||
ah.plugin.GetAPI().LogWarn("Unable to refresh the subscription", "error", err.Error()) | ||
ah.plugin.GetMetrics().ObserveLifecycleEvent(event.LifecycleEvent, metrics.DiscardedReasonFailedToRefresh) | ||
return | ||
} | ||
if event.LifecycleEvent != "reauthorizationRequired" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check the event type upfront. |
||
ah.plugin.GetMetrics().ObserveLifecycleEvent(event.LifecycleEvent, metrics.DiscardedReasonUnknownLifecycleEvent) | ||
return | ||
} | ||
|
||
ah.plugin.GetMetrics().ObserveSubscription(metrics.SubscriptionRefreshed) | ||
if err = ah.plugin.GetStore().UpdateSubscriptionExpiresOn(event.SubscriptionID, *expiresOn); err != nil { | ||
ah.plugin.GetAPI().LogWarn("Unable to store the subscription new expiry date", "subscription_id", event.SubscriptionID, "error", err.Error()) | ||
} | ||
// Ignore subscriptions we aren't tracking locally. For now, that's just the single global chats subscription. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would renew everything, even if unused -- lots of time on msteams-sync-test doing this until I upgraded to a tes build. |
||
if _, err := ah.plugin.GetStore().GetGlobalSubscription(event.SubscriptionID); err == sql.ErrNoRows { | ||
ah.plugin.GetAPI().LogWarn("Ignoring reauthorizationRequired lifecycle event for unused subscription", "subscription_id", event.SubscriptionID) | ||
ah.plugin.GetMetrics().ObserveLifecycleEvent(event.LifecycleEvent, metrics.DiscardedReasonUnusedSubscription) | ||
return | ||
} else if err != nil { | ||
ah.plugin.GetAPI().LogWarn("Failed to lookup subscription, refreshing anyway", "subscription_id", event.SubscriptionID, "error", err.Error()) | ||
} | ||
|
||
ah.plugin.GetAPI().LogWarn("Refreshing subscription", "subscription_id", event.SubscriptionID) | ||
expiresOn, err := ah.plugin.GetClientForApp().RefreshSubscription(event.SubscriptionID) | ||
if err != nil { | ||
ah.plugin.GetAPI().LogWarn("Unable to refresh the subscription", "subscription_id", event.SubscriptionID, "error", err.Error()) | ||
ah.plugin.GetMetrics().ObserveLifecycleEvent(event.LifecycleEvent, metrics.DiscardedReasonFailedToRefresh) | ||
return | ||
} | ||
|
||
ah.plugin.GetAPI().LogWarn("Refreshed subscription", "subscription_id", event.SubscriptionID, "expires_on", expiresOn.Format("2006-01-02 15:04:05.000 Z07:00")) | ||
ah.plugin.GetMetrics().ObserveSubscription(metrics.SubscriptionRefreshed) | ||
|
||
if err = ah.plugin.GetStore().UpdateSubscriptionExpiresOn(event.SubscriptionID, *expiresOn); err != nil { | ||
ah.plugin.GetAPI().LogWarn("Unable to store the subscription new expiry date", "subscription_id", event.SubscriptionID, "error", err.Error()) | ||
} | ||
|
||
ah.plugin.GetMetrics().ObserveLifecycleEvent(event.LifecycleEvent, metrics.DiscardedReasonNone) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,9 @@ | ||
### Grafana Dashboards | ||
|
||
This folder contains Grafana dashboards for use with [performance metrics](https://docs.mattermost.com/scale/performance-monitoring.html) and the MS Teams Connect plugin. | ||
This folder contains Grafana dashboards for use with [performance metrics](https://docs.mattermost.com/scale/performance-monitoring.html) and the MS Teams plugin. | ||
|
||
#### [cloud.json](cloud.json) | ||
#### [dashboard.json](dashboard.json) | ||
|
||
This dashboard is designed for use in Mattermost Cloud, filtering to a given `namespace`. Modifications will be necessary for use in self-managed environments, but the overall graphs are nevertheless a useful starting point. | ||
|
||
![cloud dashboard](cloud.png) | ||
![dashboard](dashboard.png) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From https://pkg.go.dev/net/http:
I think I broke this a while back when fixing a different endpoint.