MM-43084 update product limits through webhook and websocket (#20076)
* Define interfaces for updating subscriptions and product limits from CWS webhook.
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c7fe5e9aab
Коммит
6f87eb67fc
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/audit"
|
"github.com/mattermost/mattermost-server/v6/audit"
|
||||||
"github.com/mattermost/mattermost-server/v6/model"
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (api *API) InitCloud() {
|
func (api *API) InitCloud() {
|
||||||
@@ -486,6 +487,21 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
c.Err = appErr
|
c.Err = appErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case model.EventTypeSubscriptionChanged:
|
||||||
|
// event.ProductLimits is nil if there was no change
|
||||||
|
if event.ProductLimits != nil {
|
||||||
|
if pluginsEnvironment := c.App.GetPluginsEnvironment(); pluginsEnvironment != nil {
|
||||||
|
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||||
|
hooks.OnCloudLimitsUpdated(event.ProductLimits)
|
||||||
|
return true
|
||||||
|
}, plugin.OnCloudLimitsUpdatedID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.App.Cloud().UpdateSubscriptionFromHook(event.ProductLimits, event.Subscription); err != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.subscription.update_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Logger.Info("Updated subscription from webhook event")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
|
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
|
||||||
|
|||||||
@@ -1179,3 +1179,9 @@ func (api *PluginAPI) RequestTrialLicense(requesterID string, users int, termsAc
|
|||||||
|
|
||||||
return api.app.Channels().RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)
|
return api.app.Channels().RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCloudLimits returns any limits associated with the cloud instance
|
||||||
|
func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) {
|
||||||
|
limits, err := api.app.Cloud().GetCloudLimits("")
|
||||||
|
return limits, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1302,3 +1302,47 @@ func TestHookOnSendDailyTelemetry(t *testing.T) {
|
|||||||
|
|
||||||
require.True(t, hookCalled)
|
require.True(t, hookCalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHookOnCloudLimitsUpdated(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
|
||||||
|
[]string{
|
||||||
|
`
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MyPlugin struct {
|
||||||
|
plugin.MattermostPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MyPlugin) OnCloudLimitsUpdated(_ *model.ProductLimits) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.ClientMain(&MyPlugin{})
|
||||||
|
}
|
||||||
|
`}, th.App, th.NewPluginAPI)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
require.Len(t, pluginIDs, 1)
|
||||||
|
pluginID := pluginIDs[0]
|
||||||
|
|
||||||
|
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
|
||||||
|
|
||||||
|
hookCalled := false
|
||||||
|
th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||||
|
hooks.OnCloudLimitsUpdated(nil)
|
||||||
|
|
||||||
|
hookCalled = true
|
||||||
|
return hookCalled
|
||||||
|
}, plugin.OnCloudLimitsUpdatedID)
|
||||||
|
|
||||||
|
require.True(t, hookCalled)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
type CloudInterface interface {
|
type CloudInterface interface {
|
||||||
GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error)
|
GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error)
|
||||||
GetCloudLimits(userID string) (*model.ProductLimits, error)
|
GetCloudLimits(userID string) (*model.ProductLimits, error)
|
||||||
|
UpdateSubscriptionFromHook(*model.ProductLimits, *model.Subscription) error
|
||||||
|
|
||||||
CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error)
|
CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error)
|
||||||
ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error
|
ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error
|
||||||
|
|||||||
@@ -292,3 +292,17 @@ func (_m *CloudInterface) UpdateCloudCustomerAddress(userID string, address *mod
|
|||||||
|
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSubscriptionFromHook provides a mock function with given fields: _a0, _a1
|
||||||
|
func (_m *CloudInterface) UpdateSubscriptionFromHook(_a0 *model.ProductLimits, _a1 *model.Subscription) error {
|
||||||
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(*model.ProductLimits, *model.Subscription) error); ok {
|
||||||
|
r0 = rf(_a0, _a1)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|||||||
@@ -475,6 +475,10 @@
|
|||||||
"id": "api.cloud.request_error",
|
"id": "api.cloud.request_error",
|
||||||
"translation": "Error processing request to CWS."
|
"translation": "Error processing request to CWS."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.cloud.subscription.update_error",
|
||||||
|
"translation": "Error updating subscription from webhook."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.command.admin_only.app_error",
|
"id": "api.command.admin_only.app_error",
|
||||||
"translation": "Integrations have been limited to admins only."
|
"translation": "Integrations have been limited to admins only."
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const (
|
|||||||
EventTypeFailedPaymentNoCard = "failed-payment-no-card"
|
EventTypeFailedPaymentNoCard = "failed-payment-no-card"
|
||||||
EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email"
|
EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email"
|
||||||
EventTypeSendUpgradeConfirmationEmail = "send-upgrade-confirmation-email"
|
EventTypeSendUpgradeConfirmationEmail = "send-upgrade-confirmation-email"
|
||||||
|
EventTypeSubscriptionChanged = "subscription-changed"
|
||||||
EventTypeTrialWillEnd = "trial-will-end"
|
EventTypeTrialWillEnd = "trial-will-end"
|
||||||
EventTypeTrialEnded = "trial-ended"
|
EventTypeTrialEnded = "trial-ended"
|
||||||
)
|
)
|
||||||
@@ -166,6 +167,8 @@ type CWSWebhookPayload struct {
|
|||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
FailedPayment *FailedPayment `json:"failed_payment"`
|
FailedPayment *FailedPayment `json:"failed_payment"`
|
||||||
CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"`
|
CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"`
|
||||||
|
ProductLimits *ProductLimits `json:"product_limits"`
|
||||||
|
Subscription *Subscription `json:"subscription"`
|
||||||
SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"`
|
SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const (
|
|||||||
WebsocketWarnMetricStatusReceived = "warn_metric_status_received"
|
WebsocketWarnMetricStatusReceived = "warn_metric_status_received"
|
||||||
WebsocketWarnMetricStatusRemoved = "warn_metric_status_removed"
|
WebsocketWarnMetricStatusRemoved = "warn_metric_status_removed"
|
||||||
WebsocketEventCloudPaymentStatusUpdated = "cloud_payment_status_updated"
|
WebsocketEventCloudPaymentStatusUpdated = "cloud_payment_status_updated"
|
||||||
|
WebsocketEventCloudSubscriptionChanged = "cloud_subscription_changed"
|
||||||
WebsocketEventThreadUpdated = "thread_updated"
|
WebsocketEventThreadUpdated = "thread_updated"
|
||||||
WebsocketEventThreadFollowChanged = "thread_follow_changed"
|
WebsocketEventThreadFollowChanged = "thread_follow_changed"
|
||||||
WebsocketEventThreadReadChanged = "thread_read_changed"
|
WebsocketEventThreadReadChanged = "thread_read_changed"
|
||||||
|
|||||||
@@ -1147,6 +1147,11 @@ type API interface {
|
|||||||
//
|
//
|
||||||
// Minimum server version: 5.36
|
// Minimum server version: 5.36
|
||||||
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
|
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
|
||||||
|
|
||||||
|
// GetCloudLimits gets limits associated with a cloud workspace, if any
|
||||||
|
//
|
||||||
|
// Minimum server version: 7.0
|
||||||
|
GetCloudLimits() (*model.ProductLimits, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var handshake = plugin.HandshakeConfig{
|
var handshake = plugin.HandshakeConfig{
|
||||||
|
|||||||
@@ -1225,3 +1225,10 @@ func (api *apiTimerLayer) RequestTrialLicense(requesterID string, users int, ter
|
|||||||
api.recordTime(startTime, "RequestTrialLicense", _returnsA == nil)
|
api.recordTime(startTime, "RequestTrialLicense", _returnsA == nil)
|
||||||
return _returnsA
|
return _returnsA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *apiTimerLayer) GetCloudLimits() (*model.ProductLimits, error) {
|
||||||
|
startTime := timePkg.Now()
|
||||||
|
_returnsA, _returnsB := api.apiImpl.GetCloudLimits()
|
||||||
|
api.recordTime(startTime, "GetCloudLimits", _returnsB == nil)
|
||||||
|
return _returnsA, _returnsB
|
||||||
|
}
|
||||||
|
|||||||
@@ -774,6 +774,39 @@ func (s *hooksRPCServer) OnSendDailyTelemetry(args *Z_OnSendDailyTelemetryArgs,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hookNameToId["OnCloudLimitsUpdated"] = OnCloudLimitsUpdatedID
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_OnCloudLimitsUpdatedArgs struct {
|
||||||
|
A *model.ProductLimits
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_OnCloudLimitsUpdatedReturns struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *hooksRPCClient) OnCloudLimitsUpdated(limits *model.ProductLimits) {
|
||||||
|
_args := &Z_OnCloudLimitsUpdatedArgs{limits}
|
||||||
|
_returns := &Z_OnCloudLimitsUpdatedReturns{}
|
||||||
|
if g.implemented[OnCloudLimitsUpdatedID] {
|
||||||
|
if err := g.client.Call("Plugin.OnCloudLimitsUpdated", _args, _returns); err != nil {
|
||||||
|
g.log.Error("RPC call OnCloudLimitsUpdated to plugin failed.", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *hooksRPCServer) OnCloudLimitsUpdated(args *Z_OnCloudLimitsUpdatedArgs, returns *Z_OnCloudLimitsUpdatedReturns) error {
|
||||||
|
if hook, ok := s.impl.(interface {
|
||||||
|
OnCloudLimitsUpdated(limits *model.ProductLimits)
|
||||||
|
}); ok {
|
||||||
|
hook.OnCloudLimitsUpdated(args.A)
|
||||||
|
} else {
|
||||||
|
return encodableError(fmt.Errorf("Hook OnCloudLimitsUpdated called but not implemented."))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Z_RegisterCommandArgs struct {
|
type Z_RegisterCommandArgs struct {
|
||||||
A *model.Command
|
A *model.Command
|
||||||
}
|
}
|
||||||
@@ -5616,3 +5649,32 @@ func (s *apiRPCServer) RequestTrialLicense(args *Z_RequestTrialLicenseArgs, retu
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Z_GetCloudLimitsArgs struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_GetCloudLimitsReturns struct {
|
||||||
|
A *model.ProductLimits
|
||||||
|
B error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *apiRPCClient) GetCloudLimits() (*model.ProductLimits, error) {
|
||||||
|
_args := &Z_GetCloudLimitsArgs{}
|
||||||
|
_returns := &Z_GetCloudLimitsReturns{}
|
||||||
|
if err := g.client.Call("Plugin.GetCloudLimits", _args, _returns); err != nil {
|
||||||
|
log.Printf("RPC call to GetCloudLimits API failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
return _returns.A, _returns.B
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiRPCServer) GetCloudLimits(args *Z_GetCloudLimitsArgs, returns *Z_GetCloudLimitsReturns) error {
|
||||||
|
if hook, ok := s.impl.(interface {
|
||||||
|
GetCloudLimits() (*model.ProductLimits, error)
|
||||||
|
}); ok {
|
||||||
|
returns.A, returns.B = hook.GetCloudLimits()
|
||||||
|
returns.B = encodableError(returns.B)
|
||||||
|
} else {
|
||||||
|
return encodableError(fmt.Errorf("API GetCloudLimits called but not implemented."))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const (
|
|||||||
RunDataRetentionID = 24
|
RunDataRetentionID = 24
|
||||||
OnInstallID = 25
|
OnInstallID = 25
|
||||||
OnSendDailyTelemetryID = 26
|
OnSendDailyTelemetryID = 26
|
||||||
|
OnCloudLimitsUpdatedID = 27
|
||||||
TotalHooksID = iota
|
TotalHooksID = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -264,4 +265,9 @@ type Hooks interface {
|
|||||||
//
|
//
|
||||||
// Minimum server version: 6.5
|
// Minimum server version: 6.5
|
||||||
OnSendDailyTelemetry()
|
OnSendDailyTelemetry()
|
||||||
|
|
||||||
|
// OnCloudLimitsUpdated is invoked product limits change, for example when plan tiers change
|
||||||
|
//
|
||||||
|
// Minimum server version: 7.0
|
||||||
|
OnCloudLimitsUpdated(limits *model.ProductLimits)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,3 +206,9 @@ func (hooks *hooksTimerLayer) OnSendDailyTelemetry() {
|
|||||||
hooks.hooksImpl.OnSendDailyTelemetry()
|
hooks.hooksImpl.OnSendDailyTelemetry()
|
||||||
hooks.recordTime(startTime, "OnSendDailyTelemetry", true)
|
hooks.recordTime(startTime, "OnSendDailyTelemetry", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hooks *hooksTimerLayer) OnCloudLimitsUpdated(limits *model.ProductLimits) {
|
||||||
|
startTime := timePkg.Now()
|
||||||
|
hooks.hooksImpl.OnCloudLimitsUpdated(limits)
|
||||||
|
hooks.recordTime(startTime, "OnCloudLimitsUpdated", true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -980,6 +980,29 @@ func (_m *API) GetChannelsForTeamForUser(teamID string, userID string, includeDe
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCloudLimits provides a mock function with given fields:
|
||||||
|
func (_m *API) GetCloudLimits() (*model.ProductLimits, error) {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 *model.ProductLimits
|
||||||
|
if rf, ok := ret.Get(0).(func() *model.ProductLimits); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.ProductLimits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func() error); ok {
|
||||||
|
r1 = rf()
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetCommand provides a mock function with given fields: commandID
|
// GetCommand provides a mock function with given fields: commandID
|
||||||
func (_m *API) GetCommand(commandID string) (*model.Command, error) {
|
func (_m *API) GetCommand(commandID string) (*model.Command, error) {
|
||||||
ret := _m.Called(commandID)
|
ret := _m.Called(commandID)
|
||||||
|
|||||||
@@ -166,6 +166,11 @@ func (_m *Hooks) OnActivate() error {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnCloudLimitsUpdated provides a mock function with given fields: limits
|
||||||
|
func (_m *Hooks) OnCloudLimitsUpdated(limits *model.ProductLimits) {
|
||||||
|
_m.Called(limits)
|
||||||
|
}
|
||||||
|
|
||||||
// OnConfigurationChange provides a mock function with given fields:
|
// OnConfigurationChange provides a mock function with given fields:
|
||||||
func (_m *Hooks) OnConfigurationChange() error {
|
func (_m *Hooks) OnConfigurationChange() error {
|
||||||
ret := _m.Called()
|
ret := _m.Called()
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user