diff --git a/app/channels.go b/app/channels.go index 0b728a3cbc..ea84b478fe 100644 --- a/app/channels.go +++ b/app/channels.go @@ -77,6 +77,12 @@ type Channels struct { postReminderMut sync.Mutex postReminderTask *model.ScheduledTask + + // collectionTypes maps collection types array to the registering plugin + collectionTypes map[string][]string + // topicTypes maps topic types array to collection types + topicTypes map[string][]string + collectionAndTopicTypesMut sync.Mutex } func init() { @@ -94,9 +100,11 @@ func init() { func NewChannels(s *Server, services map[ServiceKey]any) (*Channels, error) { ch := &Channels{ - srv: s, - imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()), - uploadLockMap: map[string]bool{}, + srv: s, + imageProxy: imageproxy.MakeImageProxy(s.platform, s.httpService, s.Log()), + uploadLockMap: map[string]bool{}, + collectionTypes: map[string][]string{}, + topicTypes: map[string][]string{}, } // To get another service: diff --git a/app/collection.go b/app/collection.go new file mode 100644 index 0000000000..73fb5ce866 --- /dev/null +++ b/app/collection.go @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "net/http" + + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/shared/mlog" + "github.com/mattermost/mattermost-server/v6/utils" +) + +func (a *App) registerCollectionAndTopic(pluginID, collectionType, topicType string) error { + // we have a race condition due to multiple plugins calling this method + a.ch.collectionAndTopicTypesMut.Lock() + defer a.ch.collectionAndTopicTypesMut.Unlock() + + // check if collectionType was already registered by other plugin + for existingPluginID, existingCollectionTypes := range a.ch.collectionTypes { + if existingPluginID != pluginID && utils.StringInSlice(collectionType, existingCollectionTypes) { + return model.NewAppError("registerCollectionAndTopic", "app.collection.add_collection.exists.app_error", nil, "", http.StatusBadRequest) + } + } + + // check if topicType was already registered to other collection + for existingCollectionType, existingTopicTypes := range a.ch.topicTypes { + if existingCollectionType != collectionType && utils.StringInSlice(topicType, existingTopicTypes) { + return model.NewAppError("registerCollectionAndTopic", "app.collection.add_topic.exists.app_error", nil, "", http.StatusBadRequest) + } + } + + a.ch.collectionTypes[pluginID] = appendIfUnique(a.ch.collectionTypes[pluginID], collectionType) + a.ch.topicTypes[collectionType] = appendIfUnique(a.ch.topicTypes[collectionType], topicType) + + a.ch.srv.Log().Info("registered collection and topic type", mlog.String("plugin_id", pluginID), mlog.String("collection_type", collectionType), mlog.String("topic_type", topicType)) + return nil +} + +func appendIfUnique(slice []string, a string) []string { + if utils.StringInSlice(a, slice) { + return slice + } + return append(slice, a) +} diff --git a/app/plugin.go b/app/plugin.go index 77514f4656..3942ba7014 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -486,6 +486,11 @@ func (a *App) DisablePlugin(id string) *model.AppError { } func (ch *Channels) disablePlugin(id string) *model.AppError { + for _, collectionType := range ch.collectionTypes[id] { + delete(ch.topicTypes, collectionType) + } + delete(ch.collectionTypes, id) + pluginsEnvironment := ch.GetPluginsEnvironment() if pluginsEnvironment == nil { return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) diff --git a/app/plugin_api.go b/app/plugin_api.go index 21a33d2585..e302e6a860 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -1231,3 +1231,9 @@ func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) { limits, err := api.app.Cloud().GetCloudLimits("") return limits, err } + +// RegisterCollectionAndTopic informs the server that this plugin handles +// the given collection and topic types. +func (api *PluginAPI) RegisterCollectionAndTopic(collectionType, topicType string) error { + return api.app.registerCollectionAndTopic(api.id, collectionType, topicType) +} diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index d36c4fadc5..232eb9f9a4 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -2017,3 +2017,86 @@ func TestPluginAPIIsEnterpriseReady(t *testing.T) { assert.Equal(t, true, api.IsEnterpriseReady()) } + +func TestRegisterCollectionAndTopic(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_THREADSEVERYWHERE", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_THREADSEVERYWHERE") + th := Setup(t) + defer th.TearDown() + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.FeatureFlags.ThreadsEverywhere = true + }) + api := th.SetupPluginAPI() + + err := api.RegisterCollectionAndTopic("collection1", "topic1") + assert.NoError(t, err) + err = api.RegisterCollectionAndTopic("collection1", "topic1") + assert.NoError(t, err) + err = api.RegisterCollectionAndTopic("collection1", "topic2") + assert.NoError(t, err) + err = api.RegisterCollectionAndTopic("collection2", "topic3") + assert.NoError(t, err) + err = api.RegisterCollectionAndTopic("collection2", "topic1") + assert.Error(t, err) + + pluginCode := ` + package main + + import ( + "github.com/pkg/errors" + "github.com/mattermost/mattermost-server/v6/plugin" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) OnActivate() error { + if err := p.API.RegisterCollectionAndTopic("collectionTypeToBeRepeated", "some topic"); err != nil { + return errors.Wrap(err, "cannot register collection") + } + if err := p.API.RegisterCollectionAndTopic("some collection", "topicToBeRepeated"); err != nil { + return errors.Wrap(err, "cannot register collection") + } + return nil + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + ` + pluginDir, err := os.MkdirTemp("", "") + require.NoError(t, err) + webappPluginDir, err := os.MkdirTemp("", "") + require.NoError(t, err) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Directory = pluginDir + *cfg.PluginSettings.ClientDirectory = webappPluginDir + }) + + newPluginAPI := func(manifest *model.Manifest) plugin.API { + return th.App.NewPluginAPI(th.Context, manifest) + } + + env, err := plugin.NewEnvironment(newPluginAPI, NewDriverImpl(th.App.Srv()), pluginDir, webappPluginDir, th.App.Log(), nil) + require.NoError(t, err) + + th.App.ch.SetPluginsEnvironment(env) + + pluginID := "testplugin" + pluginManifest := `{"id": "testplugin", "server": {"executable": "backend.exe"}}` + backend := filepath.Join(pluginDir, pluginID, "backend.exe") + utils.CompileGo(t, pluginCode, backend) + + os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600) + manifest, activated, reterr := env.Activate(pluginID) + require.NoError(t, reterr) + require.NotNil(t, manifest) + require.True(t, activated) + + err = api.RegisterCollectionAndTopic("collectionTypeToBeRepeated", "some other topic") + assert.Error(t, err) + err = api.RegisterCollectionAndTopic("some other collection", "topicToBeRepeated") + assert.Error(t, err) +} diff --git a/i18n/en.json b/i18n/en.json index 710054bf1a..7171c90c47 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4815,6 +4815,14 @@ "id": "app.cloud.upgrade_plan_bot_message_single", "translation": "{{.UsersNum}} member of the {{.WorkspaceName}} workspace has requested a workspace upgrade for: " }, + { + "id": "app.collection.add_collection.exists.app_error", + "translation": "Collection type already exists." + }, + { + "id": "app.collection.add_topic.exists.app_error", + "translation": "Topic type already exists." + }, { "id": "app.command.createcommand.internal_error", "translation": "Unable to save the command." diff --git a/model/collection.go b/model/collection.go new file mode 100644 index 0000000000..2389a17edf --- /dev/null +++ b/model/collection.go @@ -0,0 +1,20 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type CollectionMetadata struct { + Id string `json:"id"` + TeamId string `json:"team_id"` + CollectionType string `json:"collection_type"` + Name string `json:"name"` + RelativeURL string `json:"relative_url"` +} + +type TopicMetadata struct { + Id string `json:"id"` + TeamId string `json:"team_id"` + TopicType string `json:"topic_type"` + CollectionType string `json:"collection_type"` + CollectionId string `json:"collection_id"` +} diff --git a/model/feature_flags.go b/model/feature_flags.go index 5330227f83..ab5563ee00 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -81,6 +81,8 @@ type FeatureFlags struct { // A/B Test on reduced onboarding task list item ReduceOnBoardingTaskList bool + + ThreadsEverywhere bool } func (f *FeatureFlags) SetDefaults() { @@ -111,6 +113,7 @@ func (f *FeatureFlags) SetDefaults() { f.PeopleProduct = false f.AnnualSubscription = false f.ReduceOnBoardingTaskList = false + f.ThreadsEverywhere = false } func (f *FeatureFlags) Plugins() map[string]string { diff --git a/plugin/api.go b/plugin/api.go index 6ccbc8c60f..d3b49da50c 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -1157,6 +1157,17 @@ type API interface { // // Minimum server version: 7.1 EnsureBotUser(bot *model.Bot) (string, error) + + // RegisterCollectionAndTopic informs the server that this plugin handles + // the given collection and topic types. + // + // It is an error for different plugins to register the same pair of types, + // or even to register a new topic against another plugin's collection. + // + // EXPERIMENTAL: This API is experimental and can be changed without advance notice. + // + // Minimum server version: 7.6 + RegisterCollectionAndTopic(collectionType, topicType string) error } var handshake = plugin.HandshakeConfig{ diff --git a/plugin/api_timer_layer_generated.go b/plugin/api_timer_layer_generated.go index 761bd0f21f..b64a2229ce 100644 --- a/plugin/api_timer_layer_generated.go +++ b/plugin/api_timer_layer_generated.go @@ -1239,3 +1239,10 @@ func (api *apiTimerLayer) EnsureBotUser(bot *model.Bot) (string, error) { api.recordTime(startTime, "EnsureBotUser", _returnsB == nil) return _returnsA, _returnsB } + +func (api *apiTimerLayer) RegisterCollectionAndTopic(collectionType, topicType string) error { + startTime := timePkg.Now() + _returnsA := api.apiImpl.RegisterCollectionAndTopic(collectionType, topicType) + api.recordTime(startTime, "RegisterCollectionAndTopic", _returnsA == nil) + return _returnsA +} diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 45dc6c0143..b442898080 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -807,6 +807,236 @@ func (s *hooksRPCServer) OnCloudLimitsUpdated(args *Z_OnCloudLimitsUpdatedArgs, return nil } +func init() { + hookNameToId["UserHasPermissionToCollection"] = UserHasPermissionToCollectionID +} + +type Z_UserHasPermissionToCollectionArgs struct { + A *Context + B string + C string + D string + E *model.Permission +} + +type Z_UserHasPermissionToCollectionReturns struct { + A bool + B error +} + +func (g *hooksRPCClient) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) { + _args := &Z_UserHasPermissionToCollectionArgs{c, userID, collectionType, collectionId, permission} + _returns := &Z_UserHasPermissionToCollectionReturns{} + if g.implemented[UserHasPermissionToCollectionID] { + if err := g.client.Call("Plugin.UserHasPermissionToCollection", _args, _returns); err != nil { + g.log.Error("RPC call UserHasPermissionToCollection to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) UserHasPermissionToCollection(args *Z_UserHasPermissionToCollectionArgs, returns *Z_UserHasPermissionToCollectionReturns) error { + if hook, ok := s.impl.(interface { + UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) + }); ok { + returns.A, returns.B = hook.UserHasPermissionToCollection(args.A, args.B, args.C, args.D, args.E) + returns.B = encodableError(returns.B) + } else { + return encodableError(fmt.Errorf("Hook UserHasPermissionToCollection called but not implemented.")) + } + return nil +} + +func init() { + hookNameToId["GetAllCollectionIDsForUser"] = GetAllCollectionIDsForUserID +} + +type Z_GetAllCollectionIDsForUserArgs struct { + A *Context + B string + C string +} + +type Z_GetAllCollectionIDsForUserReturns struct { + A []string + B error +} + +func (g *hooksRPCClient) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) { + _args := &Z_GetAllCollectionIDsForUserArgs{c, userID, collectionType} + _returns := &Z_GetAllCollectionIDsForUserReturns{} + if g.implemented[GetAllCollectionIDsForUserID] { + if err := g.client.Call("Plugin.GetAllCollectionIDsForUser", _args, _returns); err != nil { + g.log.Error("RPC call GetAllCollectionIDsForUser to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) GetAllCollectionIDsForUser(args *Z_GetAllCollectionIDsForUserArgs, returns *Z_GetAllCollectionIDsForUserReturns) error { + if hook, ok := s.impl.(interface { + GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) + }); ok { + returns.A, returns.B = hook.GetAllCollectionIDsForUser(args.A, args.B, args.C) + returns.B = encodableError(returns.B) + } else { + return encodableError(fmt.Errorf("Hook GetAllCollectionIDsForUser called but not implemented.")) + } + return nil +} + +func init() { + hookNameToId["GetAllUserIdsForCollection"] = GetAllUserIdsForCollectionID +} + +type Z_GetAllUserIdsForCollectionArgs struct { + A *Context + B string + C string +} + +type Z_GetAllUserIdsForCollectionReturns struct { + A []string + B error +} + +func (g *hooksRPCClient) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) { + _args := &Z_GetAllUserIdsForCollectionArgs{c, collectionType, collectionID} + _returns := &Z_GetAllUserIdsForCollectionReturns{} + if g.implemented[GetAllUserIdsForCollectionID] { + if err := g.client.Call("Plugin.GetAllUserIdsForCollection", _args, _returns); err != nil { + g.log.Error("RPC call GetAllUserIdsForCollection to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) GetAllUserIdsForCollection(args *Z_GetAllUserIdsForCollectionArgs, returns *Z_GetAllUserIdsForCollectionReturns) error { + if hook, ok := s.impl.(interface { + GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) + }); ok { + returns.A, returns.B = hook.GetAllUserIdsForCollection(args.A, args.B, args.C) + returns.B = encodableError(returns.B) + } else { + return encodableError(fmt.Errorf("Hook GetAllUserIdsForCollection called but not implemented.")) + } + return nil +} + +func init() { + hookNameToId["GetTopicRedirect"] = GetTopicRedirectID +} + +type Z_GetTopicRedirectArgs struct { + A *Context + B string + C string +} + +type Z_GetTopicRedirectReturns struct { + A string + B error +} + +func (g *hooksRPCClient) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) { + _args := &Z_GetTopicRedirectArgs{c, topicType, topicID} + _returns := &Z_GetTopicRedirectReturns{} + if g.implemented[GetTopicRedirectID] { + if err := g.client.Call("Plugin.GetTopicRedirect", _args, _returns); err != nil { + g.log.Error("RPC call GetTopicRedirect to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) GetTopicRedirect(args *Z_GetTopicRedirectArgs, returns *Z_GetTopicRedirectReturns) error { + if hook, ok := s.impl.(interface { + GetTopicRedirect(c *Context, topicType, topicID string) (string, error) + }); ok { + returns.A, returns.B = hook.GetTopicRedirect(args.A, args.B, args.C) + returns.B = encodableError(returns.B) + } else { + return encodableError(fmt.Errorf("Hook GetTopicRedirect called but not implemented.")) + } + return nil +} + +func init() { + hookNameToId["GetCollectionMetadataByIds"] = GetCollectionMetadataByIdsID +} + +type Z_GetCollectionMetadataByIdsArgs struct { + A *Context + B string + C []string +} + +type Z_GetCollectionMetadataByIdsReturns struct { + A map[string]*model.CollectionMetadata + B error +} + +func (g *hooksRPCClient) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) { + _args := &Z_GetCollectionMetadataByIdsArgs{c, collectionType, collectionIds} + _returns := &Z_GetCollectionMetadataByIdsReturns{} + if g.implemented[GetCollectionMetadataByIdsID] { + if err := g.client.Call("Plugin.GetCollectionMetadataByIds", _args, _returns); err != nil { + g.log.Error("RPC call GetCollectionMetadataByIds to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) GetCollectionMetadataByIds(args *Z_GetCollectionMetadataByIdsArgs, returns *Z_GetCollectionMetadataByIdsReturns) error { + if hook, ok := s.impl.(interface { + GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) + }); ok { + returns.A, returns.B = hook.GetCollectionMetadataByIds(args.A, args.B, args.C) + returns.B = encodableError(returns.B) + } else { + return encodableError(fmt.Errorf("Hook GetCollectionMetadataByIds called but not implemented.")) + } + return nil +} + +func init() { + hookNameToId["GetTopicMetadataByIds"] = GetTopicMetadataByIdsID +} + +type Z_GetTopicMetadataByIdsArgs struct { + A *Context + B string + C []string +} + +type Z_GetTopicMetadataByIdsReturns struct { + A map[string]*model.TopicMetadata + B error +} + +func (g *hooksRPCClient) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) { + _args := &Z_GetTopicMetadataByIdsArgs{c, topicType, topicIds} + _returns := &Z_GetTopicMetadataByIdsReturns{} + if g.implemented[GetTopicMetadataByIdsID] { + if err := g.client.Call("Plugin.GetTopicMetadataByIds", _args, _returns); err != nil { + g.log.Error("RPC call GetTopicMetadataByIds to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) GetTopicMetadataByIds(args *Z_GetTopicMetadataByIdsArgs, returns *Z_GetTopicMetadataByIdsReturns) error { + if hook, ok := s.impl.(interface { + GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) + }); ok { + returns.A, returns.B = hook.GetTopicMetadataByIds(args.A, args.B, args.C) + returns.B = encodableError(returns.B) + } else { + return encodableError(fmt.Errorf("Hook GetTopicMetadataByIds called but not implemented.")) + } + return nil +} + type Z_RegisterCommandArgs struct { A *model.Command } @@ -5708,3 +5938,33 @@ func (s *apiRPCServer) EnsureBotUser(args *Z_EnsureBotUserArgs, returns *Z_Ensur } return nil } + +type Z_RegisterCollectionAndTopicArgs struct { + A string + B string +} + +type Z_RegisterCollectionAndTopicReturns struct { + A error +} + +func (g *apiRPCClient) RegisterCollectionAndTopic(collectionType, topicType string) error { + _args := &Z_RegisterCollectionAndTopicArgs{collectionType, topicType} + _returns := &Z_RegisterCollectionAndTopicReturns{} + if err := g.client.Call("Plugin.RegisterCollectionAndTopic", _args, _returns); err != nil { + log.Printf("RPC call to RegisterCollectionAndTopic API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) RegisterCollectionAndTopic(args *Z_RegisterCollectionAndTopicArgs, returns *Z_RegisterCollectionAndTopicReturns) error { + if hook, ok := s.impl.(interface { + RegisterCollectionAndTopic(collectionType, topicType string) error + }); ok { + returns.A = hook.RegisterCollectionAndTopic(args.A, args.B) + returns.A = encodableError(returns.A) + } else { + return encodableError(fmt.Errorf("API RegisterCollectionAndTopic called but not implemented.")) + } + return nil +} diff --git a/plugin/hooks.go b/plugin/hooks.go index 0cf4e3c07a..a384dfbb2e 100644 --- a/plugin/hooks.go +++ b/plugin/hooks.go @@ -43,6 +43,12 @@ const ( OnInstallID = 25 OnSendDailyTelemetryID = 26 OnCloudLimitsUpdatedID = 27 + UserHasPermissionToCollectionID = 28 + GetAllUserIdsForCollectionID = 29 + GetAllCollectionIDsForUserID = 30 + GetTopicRedirectID = 31 + GetCollectionMetadataByIdsID = 32 + GetTopicMetadataByIdsID = 33 TotalHooksID = iota ) @@ -270,4 +276,64 @@ type Hooks interface { // // Minimum server version: 7.0 OnCloudLimitsUpdated(limits *model.ProductLimits) + + // UserHasPermissionToCollection determines if the given user has access to + // the given collection. Plugins are only expected to handle their own + // collections, and should return an error for unknown collections. + // + // For Threads Everywhere, products are expected to support at least the + // following permissions: create_post, edit_post, delete_post, + // edit_others_posts, and delete_others_posts. + // + // EXPERIMENTAL: This hook is experimental and can be changed without advance notice. + // + // Minimum server version: 7.6 + UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) + + // GetAllCollectionIDsForUser returns the set of collection ids to which + // the given user has access. Plugins are only expected to handle their + // own collections, and should return an error unknown types. + // + // EXPERIMENTAL: This hook is experimental and can be changed without advance notice. + // + // Minimum server version: 7.6 + GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) + + // GetAllCollectionIDsForUser returns the set of collection ids to which + // the given user has access. Plugins are only expected to handle their + // own collections, and should return an error for unknown types. + // + // EXPERIMENTAL: This hook is experimental and can be changed without advance notice. + // + // Minimum server version: 7.6 + GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) + + // GetTopicRedirect returns a relative URL to which to redirect a user + // following a topic permalink. Plugins are only expected to handle their + // own topic types, and should return an empty string and error for unknown // types. + // + // EXPERIMENTAL: This hook is experimental and can be changed without advance notice. + // + // Minimum server version: 7.6 + GetTopicRedirect(c *Context, topicType, topicID string) (string, error) + + // GetCollectionMetadataByIds returns collection metadata for the passed ids. + // Returned type is a map with keys of collectionId and value CollectionMetadata. + // Plugins are only expected to handle their own collections, and should + // return an error for unknown types. + // + // EXPERIMENTAL: This hook is experimental and can be changed without advance notice. + // + // Minimum server version: 7.6 + GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) + + // GetTopicMetadataByIds returns topic metadata for the passed ids. + // Returned type is a map with keys of topicId and value CollectionMetadata. + // Plugins are only expected to handle their own topics, and should + // return an error for unknown types. + // + // EXPERIMENTAL: This hook is experimental and can be changed without advance notice. + // + // Minimum server version: 7.6 + GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) } diff --git a/plugin/hooks_timer_layer_generated.go b/plugin/hooks_timer_layer_generated.go index 92ca2e393d..645c69b82a 100644 --- a/plugin/hooks_timer_layer_generated.go +++ b/plugin/hooks_timer_layer_generated.go @@ -212,3 +212,45 @@ func (hooks *hooksTimerLayer) OnCloudLimitsUpdated(limits *model.ProductLimits) hooks.hooksImpl.OnCloudLimitsUpdated(limits) hooks.recordTime(startTime, "OnCloudLimitsUpdated", true) } + +func (hooks *hooksTimerLayer) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.UserHasPermissionToCollection(c, userID, collectionType, collectionId, permission) + hooks.recordTime(startTime, "UserHasPermissionToCollection", _returnsB == nil) + return _returnsA, _returnsB +} + +func (hooks *hooksTimerLayer) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.GetAllCollectionIDsForUser(c, userID, collectionType) + hooks.recordTime(startTime, "GetAllCollectionIDsForUser", _returnsB == nil) + return _returnsA, _returnsB +} + +func (hooks *hooksTimerLayer) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.GetAllUserIdsForCollection(c, collectionType, collectionID) + hooks.recordTime(startTime, "GetAllUserIdsForCollection", _returnsB == nil) + return _returnsA, _returnsB +} + +func (hooks *hooksTimerLayer) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.GetTopicRedirect(c, topicType, topicID) + hooks.recordTime(startTime, "GetTopicRedirect", _returnsB == nil) + return _returnsA, _returnsB +} + +func (hooks *hooksTimerLayer) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.GetCollectionMetadataByIds(c, collectionType, collectionIds) + hooks.recordTime(startTime, "GetCollectionMetadataByIds", _returnsB == nil) + return _returnsA, _returnsB +} + +func (hooks *hooksTimerLayer) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.GetTopicMetadataByIds(c, topicType, topicIds) + hooks.recordTime(startTime, "GetTopicMetadataByIds", _returnsB == nil) + return _returnsA, _returnsB +} diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index b42cf52127..a7982c650c 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -2941,6 +2941,20 @@ func (_m *API) ReadFile(path string) ([]byte, *model.AppError) { return r0, r1 } +// RegisterCollectionAndTopic provides a mock function with given fields: collectionType, topicType +func (_m *API) RegisterCollectionAndTopic(collectionType string, topicType string) error { + ret := _m.Called(collectionType, topicType) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(collectionType, topicType) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // RegisterCommand provides a mock function with given fields: command func (_m *API) RegisterCommand(command *model.Command) error { ret := _m.Called(command) diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go index c02092a9aa..edee8f33e4 100644 --- a/plugin/plugintest/hooks.go +++ b/plugin/plugintest/hooks.go @@ -73,6 +73,119 @@ func (_m *Hooks) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, fil return r0, r1 } +// GetAllCollectionIDsForUser provides a mock function with given fields: c, userID, collectionType +func (_m *Hooks) GetAllCollectionIDsForUser(c *plugin.Context, userID string, collectionType string) ([]string, error) { + ret := _m.Called(c, userID, collectionType) + + var r0 []string + if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) []string); ok { + r0 = rf(c, userID, collectionType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*plugin.Context, string, string) error); ok { + r1 = rf(c, userID, collectionType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAllUserIdsForCollection provides a mock function with given fields: c, collectionType, collectionID +func (_m *Hooks) GetAllUserIdsForCollection(c *plugin.Context, collectionType string, collectionID string) ([]string, error) { + ret := _m.Called(c, collectionType, collectionID) + + var r0 []string + if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) []string); ok { + r0 = rf(c, collectionType, collectionID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*plugin.Context, string, string) error); ok { + r1 = rf(c, collectionType, collectionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetCollectionMetadataByIds provides a mock function with given fields: c, collectionType, collectionIds +func (_m *Hooks) GetCollectionMetadataByIds(c *plugin.Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) { + ret := _m.Called(c, collectionType, collectionIds) + + var r0 map[string]*model.CollectionMetadata + if rf, ok := ret.Get(0).(func(*plugin.Context, string, []string) map[string]*model.CollectionMetadata); ok { + r0 = rf(c, collectionType, collectionIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*model.CollectionMetadata) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*plugin.Context, string, []string) error); ok { + r1 = rf(c, collectionType, collectionIds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTopicMetadataByIds provides a mock function with given fields: c, topicType, topicIds +func (_m *Hooks) GetTopicMetadataByIds(c *plugin.Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) { + ret := _m.Called(c, topicType, topicIds) + + var r0 map[string]*model.TopicMetadata + if rf, ok := ret.Get(0).(func(*plugin.Context, string, []string) map[string]*model.TopicMetadata); ok { + r0 = rf(c, topicType, topicIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]*model.TopicMetadata) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*plugin.Context, string, []string) error); ok { + r1 = rf(c, topicType, topicIds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTopicRedirect provides a mock function with given fields: c, topicType, topicID +func (_m *Hooks) GetTopicRedirect(c *plugin.Context, topicType string, topicID string) (string, error) { + ret := _m.Called(c, topicType, topicID) + + var r0 string + if rf, ok := ret.Get(0).(func(*plugin.Context, string, string) string); ok { + r0 = rf(c, topicType, topicID) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*plugin.Context, string, string) error); ok { + r1 = rf(c, topicType, topicID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Implemented provides a mock function with given fields: func (_m *Hooks) Implemented() ([]string, error) { ret := _m.Called() @@ -299,6 +412,27 @@ func (_m *Hooks) UserHasLoggedIn(c *plugin.Context, user *model.User) { _m.Called(c, user) } +// UserHasPermissionToCollection provides a mock function with given fields: c, userID, collectionType, collectionId, permission +func (_m *Hooks) UserHasPermissionToCollection(c *plugin.Context, userID string, collectionType string, collectionId string, permission *model.Permission) (bool, error) { + ret := _m.Called(c, userID, collectionType, collectionId, permission) + + var r0 bool + if rf, ok := ret.Get(0).(func(*plugin.Context, string, string, string, *model.Permission) bool); ok { + r0 = rf(c, userID, collectionType, collectionId, permission) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*plugin.Context, string, string, string, *model.Permission) error); ok { + r1 = rf(c, userID, collectionType, collectionId, permission) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UserWillLogIn provides a mock function with given fields: c, user func (_m *Hooks) UserWillLogIn(c *plugin.Context, user *model.User) string { ret := _m.Called(c, user) diff --git a/plugin/product_hooks_generated.go b/plugin/product_hooks_generated.go index 584f6bed95..7f00e543aa 100644 --- a/plugin/product_hooks_generated.go +++ b/plugin/product_hooks_generated.go @@ -114,6 +114,30 @@ type OnCloudLimitsUpdatedIFace interface { OnCloudLimitsUpdated(limits *model.ProductLimits) } +type UserHasPermissionToCollectionIFace interface { + UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) +} + +type GetAllCollectionIDsForUserIFace interface { + GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) +} + +type GetAllUserIdsForCollectionIFace interface { + GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) +} + +type GetTopicRedirectIFace interface { + GetTopicRedirect(c *Context, topicType, topicID string) (string, error) +} + +type GetCollectionMetadataByIdsIFace interface { + GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) +} + +type GetTopicMetadataByIdsIFace interface { + GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) +} + type hooksAdapter struct { implemented map[int]struct{} productHooks any @@ -352,6 +376,60 @@ func newAdapter(productHooks any) (*hooksAdapter, error) { return nil, errors.New("hook has OnCloudLimitsUpdated method but does not implement plugin.OnCloudLimitsUpdated interface") } + // Assessing the type of the productHooks if it individually implements UserHasPermissionToCollection interface. + tt = reflect.TypeOf((*UserHasPermissionToCollectionIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[UserHasPermissionToCollectionID] = struct{}{} + } else if _, ok := ft.MethodByName("UserHasPermissionToCollection"); ok { + return nil, errors.New("hook has UserHasPermissionToCollection method but does not implement plugin.UserHasPermissionToCollection interface") + } + + // Assessing the type of the productHooks if it individually implements GetAllCollectionIDsForUser interface. + tt = reflect.TypeOf((*GetAllCollectionIDsForUserIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[GetAllCollectionIDsForUserID] = struct{}{} + } else if _, ok := ft.MethodByName("GetAllCollectionIDsForUser"); ok { + return nil, errors.New("hook has GetAllCollectionIDsForUser method but does not implement plugin.GetAllCollectionIDsForUser interface") + } + + // Assessing the type of the productHooks if it individually implements GetAllUserIdsForCollection interface. + tt = reflect.TypeOf((*GetAllUserIdsForCollectionIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[GetAllUserIdsForCollectionID] = struct{}{} + } else if _, ok := ft.MethodByName("GetAllUserIdsForCollection"); ok { + return nil, errors.New("hook has GetAllUserIdsForCollection method but does not implement plugin.GetAllUserIdsForCollection interface") + } + + // Assessing the type of the productHooks if it individually implements GetTopicRedirect interface. + tt = reflect.TypeOf((*GetTopicRedirectIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[GetTopicRedirectID] = struct{}{} + } else if _, ok := ft.MethodByName("GetTopicRedirect"); ok { + return nil, errors.New("hook has GetTopicRedirect method but does not implement plugin.GetTopicRedirect interface") + } + + // Assessing the type of the productHooks if it individually implements GetCollectionMetadataByIds interface. + tt = reflect.TypeOf((*GetCollectionMetadataByIdsIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[GetCollectionMetadataByIdsID] = struct{}{} + } else if _, ok := ft.MethodByName("GetCollectionMetadataByIds"); ok { + return nil, errors.New("hook has GetCollectionMetadataByIds method but does not implement plugin.GetCollectionMetadataByIds interface") + } + + // Assessing the type of the productHooks if it individually implements GetTopicMetadataByIds interface. + tt = reflect.TypeOf((*GetTopicMetadataByIdsIFace)(nil)).Elem() + + if ft.Implements(tt) { + a.implemented[GetTopicMetadataByIdsID] = struct{}{} + } else if _, ok := ft.MethodByName("GetTopicMetadataByIds"); ok { + return nil, errors.New("hook has GetTopicMetadataByIds method but does not implement plugin.GetTopicMetadataByIds interface") + } + return a, nil } @@ -579,3 +657,57 @@ func (a *hooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) { a.productHooks.(OnCloudLimitsUpdatedIFace).OnCloudLimitsUpdated(limits) } + +func (a *hooksAdapter) UserHasPermissionToCollection(c *Context, userID string, collectionType, collectionId string, permission *model.Permission) (bool, error) { + if _, ok := a.implemented[UserHasPermissionToCollectionID]; !ok { + panic("product hooks must implement UserHasPermissionToCollection") + } + + return a.productHooks.(UserHasPermissionToCollectionIFace).UserHasPermissionToCollection(c, userID, collectionType, collectionId, permission) + +} + +func (a *hooksAdapter) GetAllCollectionIDsForUser(c *Context, userID, collectionType string) ([]string, error) { + if _, ok := a.implemented[GetAllCollectionIDsForUserID]; !ok { + panic("product hooks must implement GetAllCollectionIDsForUser") + } + + return a.productHooks.(GetAllCollectionIDsForUserIFace).GetAllCollectionIDsForUser(c, userID, collectionType) + +} + +func (a *hooksAdapter) GetAllUserIdsForCollection(c *Context, collectionType, collectionID string) ([]string, error) { + if _, ok := a.implemented[GetAllUserIdsForCollectionID]; !ok { + panic("product hooks must implement GetAllUserIdsForCollection") + } + + return a.productHooks.(GetAllUserIdsForCollectionIFace).GetAllUserIdsForCollection(c, collectionType, collectionID) + +} + +func (a *hooksAdapter) GetTopicRedirect(c *Context, topicType, topicID string) (string, error) { + if _, ok := a.implemented[GetTopicRedirectID]; !ok { + panic("product hooks must implement GetTopicRedirect") + } + + return a.productHooks.(GetTopicRedirectIFace).GetTopicRedirect(c, topicType, topicID) + +} + +func (a *hooksAdapter) GetCollectionMetadataByIds(c *Context, collectionType string, collectionIds []string) (map[string]*model.CollectionMetadata, error) { + if _, ok := a.implemented[GetCollectionMetadataByIdsID]; !ok { + panic("product hooks must implement GetCollectionMetadataByIds") + } + + return a.productHooks.(GetCollectionMetadataByIdsIFace).GetCollectionMetadataByIds(c, collectionType, collectionIds) + +} + +func (a *hooksAdapter) GetTopicMetadataByIds(c *Context, topicType string, topicIds []string) (map[string]*model.TopicMetadata, error) { + if _, ok := a.implemented[GetTopicMetadataByIdsID]; !ok { + panic("product hooks must implement GetTopicMetadataByIds") + } + + return a.productHooks.(GetTopicMetadataByIdsIFace).GetTopicMetadataByIds(c, topicType, topicIds) + +}