Add Product Interfaces (#20403)
* app: improve plugin interfaces * add documentation * improve doc * add error id * add more info to readme
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
7c1b8cd937
Коммит
39991d236b
77
app/bot.go
77
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 <username> --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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
12
app/team.go
12
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
147
plugin/product.go
Обычный файл
147
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) {}
|
||||
114
product/README.md
Обычный файл
114
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.
|
||||
118
product/api.go
Обычный файл
118
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
|
||||
}
|
||||
10
product/doc.go
Обычный файл
10
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
|
||||
Ссылка в новой задаче
Block a user