From 39991d236b75f2b919a497b54a91c8daa8a2d6f3 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Mon, 13 Jun 2022 09:36:43 +0300 Subject: [PATCH] Add Product Interfaces (#20403) * app: improve plugin interfaces * add documentation * improve doc * add error id * add more info to readme --- app/bot.go | 77 ++++++++++++++++++++++ app/channel.go | 6 +- app/channels.go | 26 ++++++++ app/cluster.go | 5 -- app/product.go | 17 +++++ app/server.go | 17 ++++- app/team.go | 12 ++++ plugin/environment.go | 27 ++++++++ plugin/product.go | 147 ++++++++++++++++++++++++++++++++++++++++++ product/README.md | 114 ++++++++++++++++++++++++++++++++ product/api.go | 118 +++++++++++++++++++++++++++++++++ product/doc.go | 10 +++ 12 files changed, 566 insertions(+), 10 deletions(-) create mode 100644 plugin/product.go create mode 100644 product/README.md create mode 100644 product/api.go create mode 100644 product/doc.go diff --git a/app/bot.go b/app/bot.go index 920e10b36d..0c8cf7136b 100644 --- a/app/bot.go +++ b/app/bot.go @@ -16,6 +16,83 @@ import ( "github.com/mattermost/mattermost-server/v6/store" ) +const ( + internalKeyPrefix = "mmi_" + botUserKey = internalKeyPrefix + "botid" +) + +type botServiceWrapper struct { + app AppIface +} + +// EnsureBot provides similar functionality with the plugin-api BotService. It doesn't accept +// any ensureBotOptions hence it is not required for now. +// TODO: Once the focalboard migration completed, we should add this logic to the app and +// let plugin-api use the same code +func (w *botServiceWrapper) EnsureBot(c *request.Context, productID string, bot *model.Bot) (string, error) { + if bot == nil { + return "", errors.New("passed a nil bot") + } + + if bot.Username == "" { + return "", errors.New("passed a bot with no username") + } + + botIDBytes, err := w.app.GetPluginKey(productID, botUserKey) + if err != nil { + return "", err + } + + // If the bot has already been created, use it + if botIDBytes != nil { + botID := string(botIDBytes) + + // ensure existing bot is synced with what is being created + botPatch := &model.BotPatch{ + Username: &bot.Username, + DisplayName: &bot.DisplayName, + Description: &bot.Description, + } + + if _, err = w.app.PatchBot(botID, botPatch); err != nil { + return "", fmt.Errorf("failed to patch bot: %w", err) + } + + return botID, nil + } + + // Check for an existing bot user with that username. If one exists, then use that. + if user, appErr := w.app.GetUserByUsername(bot.Username); appErr == nil && user != nil { + if user.IsBot { + if appErr := w.app.SetPluginKey(productID, botUserKey, []byte(user.Id)); appErr != nil { + w.app.Srv().Log.Warn("Failed to set claimed bot user id.", mlog.String("userid", user.Id), mlog.Err(appErr)) + } + } else { + w.app.Srv().Log.Error("Product attempted to use an account that already exists. Convert user to a bot "+ + "account in the CLI by running 'mattermost user convert --bot'. If the user is an "+ + "existing user account you want to preserve, change its username and restart the Mattermost server, "+ + "after which the plugin will create a bot account with that name. For more information about bot "+ + "accounts, see https://mattermost.com/pl/default-bot-accounts", mlog.String("username", + bot.Username), + mlog.String("user_id", + user.Id), + ) + } + return user.Id, nil + } + + createdBot, err := w.app.CreateBot(c, bot) + if err != nil { + return "", fmt.Errorf("failed to create bot: %w", err) + } + + if appErr := w.app.SetPluginKey(productID, botUserKey, []byte(createdBot.UserId)); appErr != nil { + w.app.Srv().Log.Warn("Failed to set created bot user id.", mlog.String("userid", createdBot.UserId), mlog.Err(appErr)) + } + + return createdBot.UserId, nil +} + // CreateBot creates the given bot and corresponding user. func (a *App) CreateBot(c *request.Context, bot *model.Bot) (*model.Bot, *model.AppError) { vErr := bot.IsValidCreate() diff --git a/app/channel.go b/app/channel.go index c7104c7356..fd05d8ba13 100644 --- a/app/channel.go +++ b/app/channel.go @@ -25,17 +25,17 @@ type channelsWrapper struct { srv *Server } -func (s *channelsWrapper) GetDirectChannel(userID1, userID2 string) (*model.Channel, error) { +func (s *channelsWrapper) GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) { return s.srv.getDirectChannel(userID1, userID2) } // GetChannelByID gets a Channel by its ID. -func (s *channelsWrapper) GetChannelByID(channelID string) (*model.Channel, error) { +func (s *channelsWrapper) GetChannelByID(channelID string) (*model.Channel, *model.AppError) { return s.srv.getChannel(channelID) } // GetChannelMember gets a channel member by userID. -func (s *channelsWrapper) GetChannelMember(channelID string, userID string) (*model.ChannelMember, error) { +func (s *channelsWrapper) GetChannelMember(channelID string, userID string) (*model.ChannelMember, *model.AppError) { return s.srv.getChannelMember(context.Background(), channelID, userID) } diff --git a/app/channels.go b/app/channels.go index cd9aa3d7dd..4ecbbe2c23 100644 --- a/app/channels.go +++ b/app/channels.go @@ -16,6 +16,7 @@ import ( "github.com/mattermost/mattermost-server/v6/einterfaces" "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/plugin" + "github.com/mattermost/mattermost-server/v6/product" "github.com/mattermost/mattermost-server/v6/services/imageproxy" "github.com/mattermost/mattermost-server/v6/shared/filestore" "github.com/mattermost/mattermost-server/v6/shared/mlog" @@ -231,6 +232,18 @@ func NewChannels(s *Server, services map[ServiceKey]interface{}) (*Channels, err app: &App{ch: ch}, } + services[TeamKey] = &teamServiceWrapper{ + app: &App{ch: ch}, + } + + services[BotKey] = &botServiceWrapper{ + app: &App{ch: ch}, + } + + services[HooksKey] = &hooksService{ + ch: ch, + } + return ch, nil } @@ -307,3 +320,16 @@ func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAcce return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted) } + +type hooksService struct { + ch *Channels +} + +func (s *hooksService) RegisterHooks(productID string, hooks product.Hooks) error { + if s.ch.pluginsEnvironment == nil { + return errors.New("could not find plugins environment") + } + + s.ch.pluginsEnvironment.AddProduct(productID, hooks) + return nil +} diff --git a/app/cluster.go b/app/cluster.go index 07dd032471..07cf2b95a4 100644 --- a/app/cluster.go +++ b/app/cluster.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/mattermost/mattermost-server/v6/model" - "github.com/mattermost/mattermost-server/v6/shared/mlog" ) type clusterWrapper struct { @@ -53,10 +52,6 @@ func (s *clusterWrapper) SetPluginKeyWithOptions(productID string, key string, v return s.srv.setPluginKeyWithOptions(productID, key, value, options) } -func (s *clusterWrapper) LogError(productID, msg string, keyValuePairs ...interface{}) { - s.srv.Log.Error(msg, mlog.String("product_id", productID), mlog.Map("key-value pairs", keyValuePairs)) -} - func (s *clusterWrapper) KVGet(productID, key string) ([]byte, *model.AppError) { return s.srv.getPluginKey(productID, key) } diff --git a/app/product.go b/app/product.go index 2e09f137c8..0412ca2e8e 100644 --- a/app/product.go +++ b/app/product.go @@ -6,6 +6,8 @@ package app import ( "fmt" "strings" + + "github.com/mattermost/mattermost-server/v6/shared/mlog" ) type Product interface { @@ -78,3 +80,18 @@ func (s *Server) initializeProducts( return nil } + +type logWrapper struct { + srv *Server +} + +func (s *logWrapper) LogError(productID, msg string, keyValuePairs ...interface{}) { + s.srv.Log.Error(msg, mlog.String("product_id", productID), mlog.Map("key-value pairs", keyValuePairs)) +} + +func (s *logWrapper) LogWarn(productID, msg string, keyValuePairs ...interface{}) { + s.srv.Log.Warn(msg, mlog.String("product_id", productID), mlog.Map("key-value pairs", keyValuePairs)) +} +func (s *logWrapper) LogDebug(productID, msg string, keyValuePairs ...interface{}) { + s.srv.Log.Debug(msg, mlog.String("product_id", productID), mlog.Map("key-value pairs", keyValuePairs)) +} diff --git a/app/server.go b/app/server.go index 185e99546f..546af6477c 100644 --- a/app/server.go +++ b/app/server.go @@ -96,6 +96,9 @@ const ( UserKey ServiceKey = "user" PermissionsKey ServiceKey = "permissions" RouterKey ServiceKey = "router" + BotKey ServiceKey = "bot" + LogKey ServiceKey = "log" + HooksKey ServiceKey = "hooks" ) type Server struct { @@ -394,8 +397,10 @@ func NewServer(options ...Option) (*Server, error) { LicenseKey: s.licenseWrapper, FilestoreKey: s.filestore, ClusterKey: s.clusterWrapper, - TeamKey: s.teamService, - UserKey: s.userService, + UserKey: New(ServerConnector(s.Channels())), + LogKey: &logWrapper{ + srv: s, + }, } // Step 8: Initialize products. @@ -1174,7 +1179,15 @@ func stripPort(hostport string) string { func (s *Server) Start() error { // Start products. // This needs to happen before because products are dependent on the HTTP server. + + // make sure channels starts first + if err := s.products["channels"].Start(); err != nil { + return errors.Wrap(err, "Unable to start channels") + } for name, product := range s.products { + if name == "channels" { + continue + } if err := product.Start(); err != nil { return errors.Wrapf(err, "Unable to start %s", name) } diff --git a/app/team.go b/app/team.go index ce1d3bda7b..6fd8c35536 100644 --- a/app/team.go +++ b/app/team.go @@ -30,6 +30,18 @@ import ( "github.com/mattermost/mattermost-server/v6/store/sqlstore" ) +type teamServiceWrapper struct { + app AppIface +} + +func (w *teamServiceWrapper) GetMember(teamID, userID string) (*model.TeamMember, error) { + return w.app.GetTeamMember(teamID, userID) +} + +func (w *teamServiceWrapper) CreateMember(ctx *request.Context, teamID, userID string) (*model.TeamMember, error) { + return w.app.AddTeamMember(ctx, teamID, userID) +} + func (a *App) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError { maxActiveTeams := *teamLimits.Active teams, appErr := a.GetAllTeams() diff --git a/plugin/environment.go b/plugin/environment.go index 6e122662b4..c5477b8612 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -50,6 +50,7 @@ type PrepackagedPlugin struct { // of active plugins. type Environment struct { registeredPlugins sync.Map + registeredProducts sync.Map pluginHealthCheckJob *PluginHealthCheckJob logger *mlog.Logger metrics einterfaces.MetricsInterface @@ -300,6 +301,14 @@ func (env *Environment) Activate(id string) (manifest *model.Manifest, activated return pluginInfo.Manifest, true, nil } +func (env *Environment) AddProduct(productID string, hooks ProductHooks) { + env.registeredProducts.Store(productID, newRegisteredProduct(productID, hooks)) +} + +func (env *Environment) RemoveProduct(productID string) { + env.registeredProducts.Delete(productID) +} + func (env *Environment) RemovePlugin(id string) { if _, ok := env.registeredPlugins.Load(id); ok { env.registeredPlugins.Delete(id) @@ -481,6 +490,24 @@ func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool return result }) + env.registeredProducts.Range(func(key, value interface{}) bool { + rp := value.(*registeredProduct) + + if !rp.Implements(hookId) { + return true + } + + hookStartTime := time.Now() + result := hookRunnerFunc(rp.adapter) + + if env.metrics != nil { + elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second) + env.metrics.ObservePluginMultiHookIterationDuration(rp.productID, elapsedTime) + } + + return result + }) + if env.metrics != nil { elapsedTime := float64(time.Since(startTime)) / float64(time.Second) env.metrics.ObservePluginMultiHookDuration(elapsedTime) diff --git a/plugin/product.go b/plugin/product.go new file mode 100644 index 0000000000..18f5678b37 --- /dev/null +++ b/plugin/product.go @@ -0,0 +1,147 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "errors" + "io" + "net/http" + + "github.com/mattermost/mattermost-server/v6/model" +) + +// ProductHooks is a subset of Hooks +type ProductHooks interface { + OnConfigurationChange() error + MessageWillBePosted(ctx *Context, post *model.Post) (*model.Post, string) + MessageWillBeUpdated(ctx *Context, newPost, oldPost *model.Post) (*model.Post, string) + OnPluginClusterEvent(ctx *Context, ev model.PluginClusterEvent) + OnWebSocketDisconnect(webConnID, userID string) + OnWebSocketConnect(webConnID, userID string) + WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) +} + +type registeredProduct struct { + productID string + implemented map[int]struct{} + adapter Hooks +} + +func (rp *registeredProduct) Implements(hookId int) bool { + _, ok := rp.implemented[hookId] + return ok +} + +type hooksAdapter struct { + productHooks ProductHooks +} + +func newRegisteredProduct(pluginID string, productHooks ProductHooks) *registeredProduct { + return ®isteredProduct{ + productID: pluginID, + implemented: map[int]struct{}{ + OnConfigurationChangeID: {}, + MessageWillBePostedID: {}, + MessageWillBeUpdatedID: {}, + OnPluginClusterEventID: {}, + OnWebSocketConnectID: {}, + OnWebSocketDisconnectID: {}, + WebSocketMessageHasBeenPostedID: {}, + }, + adapter: &hooksAdapter{ + productHooks: productHooks, + }, + } +} + +func (a *hooksAdapter) OnActivate() error { + return errors.New("not implemented") +} + +func (a *hooksAdapter) Implemented() ([]string, error) { + return nil, errors.New("not implemented") +} + +func (a *hooksAdapter) OnDeactivate() error { + return errors.New("not implemented") +} + +func (a *hooksAdapter) OnConfigurationChange() error { + return a.productHooks.OnConfigurationChange() +} + +func (a *hooksAdapter) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {} + +func (a *hooksAdapter) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + return nil, model.NewAppError("ExecuteCommand", "api.command.execute_command.start.app_error", nil, "not implemented", http.StatusNotImplemented) +} + +func (a *hooksAdapter) UserHasBeenCreated(c *Context, user *model.User) {} + +func (a *hooksAdapter) UserWillLogIn(c *Context, user *model.User) string { + return "" +} + +func (a *hooksAdapter) UserHasLoggedIn(c *Context, user *model.User) {} + +func (a *hooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) { + return a.productHooks.MessageWillBePosted(c, post) +} + +func (a *hooksAdapter) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) { + return a.productHooks.MessageWillBeUpdated(c, newPost, oldPost) +} + +func (a *hooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) {} + +func (a *hooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {} + +func (a *hooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) {} + +func (a *hooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { +} + +func (a *hooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) { +} + +func (a *hooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) { +} + +func (a *hooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {} + +func (a *hooksAdapter) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) { + return nil, "" +} + +func (a *hooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {} + +func (a *hooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {} + +func (a *hooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) { + a.productHooks.OnPluginClusterEvent(c, ev) +} + +func (a *hooksAdapter) OnWebSocketConnect(webConnID, userID string) { + a.productHooks.OnWebSocketConnect(webConnID, userID) +} + +func (a *hooksAdapter) OnWebSocketDisconnect(webConnID, userID string) { + a.productHooks.OnWebSocketDisconnect(webConnID, userID) +} + +func (a *hooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) { + a.productHooks.WebSocketMessageHasBeenPosted(webConnID, userID, req) +} + +func (a *hooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) { + return -1, errors.New("not implemented") +} + +func (a *hooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error { + return errors.New("not implemented") +} + +func (a *hooksAdapter) OnSendDailyTelemetry() {} + +func (a *hooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) {} diff --git a/product/README.md b/product/README.md new file mode 100644 index 0000000000..2f14d0b47f --- /dev/null +++ b/product/README.md @@ -0,0 +1,114 @@ +# Product + +Package product defines the interfaces provided in the multi-product architecture framework. The service interfaces are designed to be a drop in replacement for services defined in the https://github.com/mattermost/mattermost-plugin-api project. Due to limitations such as the use of https://github.com/mattermost/mattermost-server/blob/master/plugin/api.go emerged this new API. Our hope is to use a single API definition or maybe even more interesting solutions like using the app.AppIFace instead (temporarily). + +## Multi-product architecture framework + +The main goal of multi-product architecture effort is to divide the prominent “app” package into sub packages so that we can maintain the complexity and lay the groundwork for future scaling opportunities. And the framework is the implementation of this idea. Currently the framework is very early to be stable and it's going to be evolve in time once we start using it. + +### How does the framework work? + +A product should conform to the following interface: + +```Go +type Product interface { + Start() error + Stop() error +} +``` + +The `app.Server` will take care of starting and stopping products. The product shall register itself via a function called `RegisterProduct` provided by `github.com/mattermost/mattermost-server/v6/app` package. To register a product, +a product initializer is required. The signature of a product initializer is defined as following: + +```Go +type app.ProductManifest struct { + Initializer func(*app.Server, map[app.ServiceKey]interface{}) (app.Product, error) + Dependencies map[app.ServiceKey]struct{} +} +``` + +Note that adding dependencies is crucial to let product framework sort product initialization. For example Channels product provides the `product.PostService` implementation therefore it should be initialized before the Boards product since it requires the PostService. An example registration could be depicted as following: + +```Go +func init() { + app.RegisterProduct("focalboard", app.ProductManifest{ + Initializer: NewBoards, + Dependencies: map[app.ServiceKey]struct{}{ + app.PostKey: {}, + app.PermissionsKey: {}, + app.UserKey: {}, + ... + }, + }) +} +``` + +### Adding services to the framework + +A product can provide services to the framework. In fact, `Channels` product provides many services by itself, so it will only need to register the service to `services` map provided by the product initializer. An example of registering a service to the "registry" is shown below: + +```Go +func NewChannels(*app.Server, map[app.ServiceKey]interface{}) (app.Product, error){ + ... + services[app.PostKey] = &postService{ + ... + } + ... +} +``` + +To improve the developer experience, you should also add the service interface to the [api definition](api.go) so that a consumer of the service can explore the methods available to them. Another good practice would be to add the servie key to the [server.go](../app/server.go) file. + +### How does a product get initialized? + +The overall server initialization starts with essential components such as the store, config etc. Right after that we start to initialize the services which are either a standalone service such as the `FileStore` and `UserService` or some services which are eventually wrappers to the server struct itself such as `ClusterService` and `LicenseService`. And the initial service map is created after these stages. + +```Go +func NewServer(options ...Option) (*Server, error) { + ... + s := &Server{} + ... + serviceMap := map[ServiceKey]interface{}{ + ... + } + + if err := s.initializeProducts(products, serviceMap); err != nil { + return nil, errors.Wrap(err, "failed to initialize products") + } + ... +} +``` + +And the product initialization is figured out by a trial and error fashion hence it is done by a maximum possible trials of initialization attempts. The order is not determined elsewhere therefore we do a on the fly sorting here. Which means the initialization order will be resolved during the loop. We have dependencies defined in the product manifest defined above. During the initialization we check if the serviceMap has all the dependencies registered. If not, we continue to the try initialize other products and register their services if they have any. + +### How to add a product to the mattermost-server? + +We don't need to define a product dependency in the `go.mod` file, we can leverage the [module workspaces](https://go.dev/ref/mod#workspaces) here. You can get more info about how we use it [here](https://docs.google.com/document/d/1Uwg_dTSNR9mx9ZDx-7osjlD4n3w13cnG6Kpz3ZGXzsM). We create another file such as `go.work`, and add the dependency there as following: + +``` +go 1.18 +use ./ +use ../sample-product +``` + +This tells the compiler to include `sample-product` to be compiled with the mattermost-server. And in order to trigger `init()` function of a product we add an empty import to a file as following: + +```Go +package imports + +import ( + ... + // Product Imports + _ "github.com/mattermost/focalboard/product" +) +``` + +### Frequently asked questions + +#### Can a product use app.App instead of services? + +Theoretically yes, but you shouldn't. The reason is we want to figure out the common entry points and use cases for the services so that we can divide the App into meaningful and functional sub services. The current service interfaces are great example of how we want to use the services among the products. + +#### How to handle circular dependency of two products? + +We are not expecting this is a requirement for the initial phase, once we complete the first product migration we can think start thinking about this. The first attempt would be to increase the granularity of the initialization phase by adopting service initialization resolution. So that a service can be initialized even a product initialization starts. diff --git a/product/api.go b/product/api.go new file mode 100644 index 0000000000..a59af13abe --- /dev/null +++ b/product/api.go @@ -0,0 +1,118 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package product + +import ( + "github.com/gorilla/mux" + "github.com/mattermost/mattermost-server/v6/app/request" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/mattermost/mattermost-server/v6/plugin" +) + +// RouterService enables registering the product router to the server. After registering the +// router, the ServeHTTP hook which was being used in plugin mode is not required anymore. +// For now, the service implementation is provided by Channels therefore the consumer products +// should add this service key to their dependencies map in the app.ProductManifest. +// +// The service shall be registered via app.RouterKey service key. +type RouterService interface { + RegisterRouter(productID string, sub *mux.Router) +} + +// PostService provides posts related utilities. For now, the service implementation +// is provided by Channels therefore the consumer products should add this service key to +// their dependencies map in the app.ProductManifest. +// +// The service shall be registered via app.PostKey service key. +type PostService interface { + CreatePost(context *request.Context, post *model.Post) (*model.Post, *model.AppError) +} + +// PermissionService provides permissions related utilities. For now, the service implementation +// is provided by Channels therefore the consumer products should add this service key to their +// dependencies map in the app.ProductManifest. +// +// The service shall be registered via app.PermissionKey service key. +type PermissionService interface { + HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool +} + +// ClusterService enables to publish cluster events. In addition to that, It's being used for +// mattermost-plugin-api Mutex API with the SetPluginKeyWithOptions method. +// +// The service shall be registered via app.ClusterKey key. +type ClusterService interface { + PublishPluginClusterEvent(productID string, ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error + PublishWebSocketEvent(productID string, event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) + SetPluginKeyWithOptions(productID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) +} + +// ChannelService provides channel related API The service implementation is provided by +// Channels product therefore the consumer products should add this service key to their +// dependencies map in the app.ProductManifest. +// +// The service shall be registered via app.ChannelKey service key. +type ChannelService interface { + GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) + GetChannelByID(channelID string) (*model.Channel, *model.AppError) + GetChannelMember(channelID string, userID string) (*model.ChannelMember, *model.AppError) +} + +// LicenseService provides license related utilities. +// +// The service shall be registered via app.LicenseKey service key. +type LicenseService interface { + GetLicense() *model.License + RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError +} + +// UserService provides user related utilities. Initially this was thought to be app/users.UserService +// but it's replaced by app.App temporarily. The reason is; UserService is a standalone tool whereas the +// existing plugin API was using channels related app functionalities as well. We shall improve the UserService +// to meet emerging requirements. +// +// The service shall be registered via app.UserKey service key. +type UserService interface { + GetUser(userID string) (*model.User, *model.AppError) + UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) + GetUserByEmail(email string) (*model.User, *model.AppError) + GetUserByUsername(username string) (*model.User, *model.AppError) +} + +// TeamService provides team related utilities. +// +// The service shall be registered via app.TeamKey service key. +type TeamService interface { + GetMember(teamID, userID string) (*model.TeamMember, error) + CreateMember(ctx *request.Context, teamID, userID string) (*model.TeamMember, error) +} + +// BotService is just a copy implementation of mattermost-plugin-api EnsureBot method. +// +// The service shall be registered via app.BotKey service key. +type BotService interface { + EnsureBot(ctx *request.Context, productID string, bot *model.Bot) (string, error) +} + +// LogService shall be registered via app.LogKey service key. +type LogService interface { + LogError(productID, msg string, keyValuePairs ...interface{}) + LogWarn(productID, msg string, keyValuePairs ...interface{}) + LogDebug(productID, msg string, keyValuePairs ...interface{}) +} + +// Hooks is an interim solution for enabling plugin hooks on the multi-product architecture. After the +// focalboard migration is completed, this API should replaced with something else that would enable a +// product to register any hook. Currently this is added to unblock the migration. +type Hooks interface { + plugin.ProductHooks +} + +// HooksService is the API for adding exiting plugin hooks to the server so that they can be called as +// they were. This Service is required to be used after the products start. Otherwise it will return an error. +// +// The service shall be registered via app.HooksKey service key. +type HooksService interface { + RegisterHooks(productID string, hooks Hooks) error +} diff --git a/product/doc.go b/product/doc.go new file mode 100644 index 0000000000..39df413813 --- /dev/null +++ b/product/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// Package product defines the interfaces provided in the multi-product architecture +// framework. The service interfaces are designed to be a drop in replacement for services +// defined in the https://github.com/mattermost/mattermost-plugin-api project. Due to limitations +// such as the use of https://github.com/mattermost/mattermost-server/blob/master/plugin/api.go +// emerged this new API. Our hope is to use a single API definition or maybe even more interesting +// solutions like using the app.AppIFace instead. +package product