[MM-53156] Remove Multi-Product architecture (#25669)

Этот коммит содержится в:
Ben Schumacher
2024-02-15 13:01:44 +01:00
коммит произвёл GitHub
родитель d3b799eaa5
Коммит de3e5aab25
42 изменённых файлов: 86 добавлений и 2240 удалений

Просмотреть файл

@@ -299,8 +299,7 @@ func SetupConfig(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelpe
}
func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper {
setupOptions := []app.Option{app.SkipProductsInitialization()}
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, updateConfig, setupOptions)
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, updateConfig, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
@@ -314,8 +313,7 @@ func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config
}
func SetupWithStoreMock(tb testing.TB) *TestHelper {
setupOptions := []app.Option{app.SkipProductsInitialization()}
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, nil, setupOptions)
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, nil, nil)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
@@ -329,8 +327,7 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper {
}
func SetupEnterpriseWithStoreMock(tb testing.TB, options ...app.Option) *TestHelper {
setupOptions := append(options, app.SkipProductsInitialization())
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, true, false, nil, setupOptions)
th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, true, false, nil, options)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)

Просмотреть файл

@@ -12,7 +12,6 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/timezones"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/einterfaces"
"github.com/mattermost/mattermost/server/v8/platform/services/httpservice"
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
@@ -155,15 +154,3 @@ func (a *App) SetServer(srv *Server) {
func (a *App) UpdateExpiredDNDStatuses() ([]*model.Status, error) {
return a.Srv().Store().Status().UpdateExpiredDNDStatuses()
}
// Ensure system service adapter implements `product.SystemService`
var _ product.SystemService = (*systemServiceAdapter)(nil)
// systemServiceAdapter provides a collection of system APIs for use by products.
type systemServiceAdapter struct {
server *Server
}
func (ssa *systemServiceAdapter) GetDiagnosticId() string {
return ssa.server.TelemetryId()
}

Просмотреть файл

@@ -140,9 +140,7 @@ type AppIface interface {
EnablePlugin(id string) *model.AppError
// 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
EnsureBot(rctx request.CTX, productID string, bot *model.Bot) (string, error)
EnsureBot(rctx request.CTX, pluginID string, bot *model.Bot) (string, error)
// Expand announcements in incoming webhooks from Slack. Those announcements
// can be found in the text attribute, or in the pretext, text, title and value
// attributes of the attachment structure. The Slack attachment structure is

Просмотреть файл

@@ -14,7 +14,6 @@ import (
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
@@ -23,23 +22,9 @@ const (
botUserKey = internalKeyPrefix + "botid"
)
// Ensure bot service wrapper implements `product.BotService`
var _ product.BotService = (*botServiceWrapper)(nil)
// botServiceWrapper provides an implementation of `product.BotService` for use by products.
type botServiceWrapper struct {
app AppIface
}
func (w *botServiceWrapper) EnsureBot(c request.CTX, productID string, bot *model.Bot) (string, error) {
return w.app.EnsureBot(c, productID, bot)
}
// 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 (a *App) EnsureBot(rctx request.CTX, productID string, bot *model.Bot) (string, error) {
func (a *App) EnsureBot(rctx request.CTX, pluginID string, bot *model.Bot) (string, error) {
if bot == nil {
return "", errors.New("passed a nil bot")
}
@@ -48,7 +33,7 @@ func (a *App) EnsureBot(rctx request.CTX, productID string, bot *model.Bot) (str
return "", errors.New("passed a bot with no username")
}
botIDBytes, err := a.GetPluginKey(productID, botUserKey)
botIDBytes, err := a.GetPluginKey(pluginID, botUserKey)
if err != nil {
return "", err
}
@@ -74,11 +59,11 @@ func (a *App) EnsureBot(rctx request.CTX, productID string, bot *model.Bot) (str
// Check for an existing bot user with that username. If one exists, then use that.
if user, appErr := a.GetUserByUsername(bot.Username); appErr == nil && user != nil {
if user.IsBot {
if appErr := a.SetPluginKey(productID, botUserKey, []byte(user.Id)); appErr != nil {
if appErr := a.SetPluginKey(pluginID, botUserKey, []byte(user.Id)); appErr != nil {
return "", fmt.Errorf("failed to set plugin key: %w", err)
}
} else {
rctx.Logger().Error("Product attempted to use an account that already exists. Convert user to a bot "+
rctx.Logger().Error("Plugin 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 "+
@@ -96,7 +81,7 @@ func (a *App) EnsureBot(rctx request.CTX, productID string, bot *model.Bot) (str
return "", fmt.Errorf("failed to create bot: %w", err)
}
if appErr := a.SetPluginKey(productID, botUserKey, []byte(createdBot.UserId)); appErr != nil {
if appErr := a.SetPluginKey(pluginID, botUserKey, []byte(createdBot.UserId)); appErr != nil {
return "", fmt.Errorf("failed to set plugin key: %w", err)
}

Просмотреть файл

@@ -19,7 +19,6 @@ import (
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
pUtils "github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
)
@@ -28,89 +27,6 @@ const (
UpdateMultipleMaximum = 200
)
// channelsWrapper provides an implementation of `product.ChannelService` to be used by products.
type channelsWrapper struct {
app *App
}
func (s *channelsWrapper) GetDirectChannel(userID1, userID2 string) (*model.Channel, *model.AppError) {
return s.app.getDirectChannel(request.EmptyContext(s.app.Log()), userID1, userID2)
}
// GetChannelByID gets a Channel by its ID.
func (s *channelsWrapper) GetChannelByID(channelID string) (*model.Channel, *model.AppError) {
return s.app.GetChannel(request.EmptyContext(s.app.Log()), channelID)
}
// GetChannelMember gets a channel member by userID.
func (s *channelsWrapper) GetChannelMember(channelID string, userID string) (*model.ChannelMember, *model.AppError) {
return s.app.GetChannelMember(request.EmptyContext(s.app.Log()), channelID, userID)
}
func (s *channelsWrapper) GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError) {
return s.app.GetChannelsForTeamForUser(request.EmptyContext(s.app.Log()), teamID, userID, opts)
}
func (s *channelsWrapper) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) {
return s.app.GetSidebarCategoriesForTeamForUser(request.EmptyContext(s.app.Log()), userID, teamID)
}
func (s *channelsWrapper) GetChannelMembers(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError) {
return s.app.GetChannelMembersPage(request.EmptyContext(s.app.Log()), channelID, page, perPage)
}
func (s *channelsWrapper) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) {
return s.app.CreateSidebarCategory(request.EmptyContext(s.app.Log()), userID, teamID, newCategory)
}
func (s *channelsWrapper) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) {
return s.app.UpdateSidebarCategories(request.EmptyContext(s.app.Log()), userID, teamID, categories)
}
func (s *channelsWrapper) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
return s.app.CreateChannel(request.EmptyContext(s.app.Log()), channel, false)
}
func (s *channelsWrapper) AddUserToChannel(channelID, userID, asUserID string) (*model.ChannelMember, *model.AppError) {
ctx := request.EmptyContext(s.app.Log())
channel, err := s.app.GetChannel(ctx, channelID)
if err != nil {
return nil, err
}
return s.app.AddChannelMember(ctx, userID, channel, ChannelMemberOpts{
UserRequestorID: asUserID,
})
}
func (s *channelsWrapper) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, *model.AppError) {
return s.app.UpdateChannelMemberRoles(request.EmptyContext(s.app.Log()), channelID, userID, newRoles)
}
func (s *channelsWrapper) DeleteChannelMember(channelID, userID string) *model.AppError {
return s.app.LeaveChannel(request.EmptyContext(s.app.Log()), channelID, userID)
}
func (s *channelsWrapper) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) {
channel, err := s.GetChannelByID(channelID)
if err != nil {
return nil, err
}
return s.app.AddChannelMember(request.EmptyContext(s.app.Log()), userID, channel, ChannelMemberOpts{
// For now, don't allow overriding these via the plugin API.
UserRequestorID: "",
PostRootID: "",
})
}
func (s *channelsWrapper) GetDirectChannelOrCreate(userID1, userID2 string) (*model.Channel, *model.AppError) {
return s.app.GetOrCreateDirectChannel(request.EmptyContext(s.app.Log()), userID1, userID2)
}
// Ensure the wrapper implements the product service.
var _ product.ChannelService = (*channelsWrapper)(nil)
// DefaultChannelNames returns the list of system-wide default channel names.
//
// By default the list will be (not necessarily in this order):

Просмотреть файл

@@ -4,7 +4,6 @@
package app
import (
"fmt"
"runtime"
"strings"
"sync"
@@ -16,31 +15,26 @@ import (
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/config"
"github.com/mattermost/mattermost/server/v8/einterfaces"
"github.com/mattermost/mattermost/server/v8/platform/services/imageproxy"
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
)
const ServerKey product.ServiceKey = "server"
// licenseSvc is added to act as a starting point for future integrated products.
// It has the same signature and functionality with the license related APIs of the plugin-api.
type licenseSvc interface {
GetLicense() *model.License
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError
type configService interface {
Config() *model.Config
AddConfigListener(listener func(*model.Config, *model.Config)) string
RemoveConfigListener(id string)
UpdateConfig(f func(*model.Config))
SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError)
}
// Channels contains all channels related state.
type Channels struct {
srv *Server
cfgSvc product.ConfigService
cfgSvc configService
filestore filestore.FileBackend
exportFilestore filestore.FileBackend
licenseSvc licenseSvc
routerSvc *routerService
postActionCookieSecret []byte
@@ -84,76 +78,16 @@ type Channels struct {
postReminderTask *model.ScheduledTask
}
func init() {
product.RegisterProduct("channels", product.Manifest{
Initializer: func(services map[product.ServiceKey]any) (product.Product, error) {
return NewChannels(services)
},
Dependencies: map[product.ServiceKey]struct{}{
ServerKey: {},
product.ConfigKey: {},
product.LicenseKey: {},
product.FilestoreKey: {},
product.ExportFilestoreKey: {},
},
})
}
func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
s, ok := services[ServerKey].(*Server)
if !ok {
return nil, errors.New("server not passed")
}
func NewChannels(s *Server) (*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{},
filestore: s.FileBackend(),
exportFilestore: s.ExportFileBackend(),
cfgSvc: s.Platform(),
}
// To get another service:
// 1. Prepare the service interface
// 2. Add the field to *Channels
// 3. Add the service key to the slice.
// 4. Add a new case in the switch statement.
requiredServices := []product.ServiceKey{
product.ConfigKey,
product.LicenseKey,
product.FilestoreKey,
product.ExportFilestoreKey,
}
for _, svcKey := range requiredServices {
svc, ok := services[svcKey]
if !ok {
return nil, fmt.Errorf("Service %s not passed", svcKey)
}
switch svcKey {
// Keep adding more services here
case product.ConfigKey:
cfgSvc, ok := svc.(product.ConfigService)
if !ok {
return nil, errors.New("Config service did not satisfy ConfigSvc interface")
}
ch.cfgSvc = cfgSvc
case product.FilestoreKey:
filestore, ok := svc.(filestore.FileBackend)
if !ok {
return nil, errors.New("Filestore service did not satisfy FileBackend interface")
}
ch.filestore = filestore
case product.ExportFilestoreKey:
exportFilestore, ok := svc.(filestore.FileBackend)
if !ok {
return nil, errors.New("Export filestore service did not satisfy FileBackend interface")
}
ch.exportFilestore = exportFilestore
case product.LicenseKey:
svc, ok := svc.(licenseSvc)
if !ok {
return nil, errors.New("License service did not satisfy licenseSvc interface")
}
ch.licenseSvc = svc
}
}
// We are passing a partially filled Channels struct so that the enterprise
// methods can have access to app methods.
// Otherwise, passing server would mean it has to call s.Channels(),
@@ -207,45 +141,12 @@ func NewChannels(services map[product.ServiceKey]any) (*Channels, error) {
return nil, errors.Wrap(imgErr, "failed to create image encoder")
}
ch.routerSvc = newRouterService()
services[product.RouterKey] = ch.routerSvc
// Setup routes.
pluginsRoute := ch.srv.Router.PathPrefix("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter()
pluginsRoute.HandleFunc("", ch.ServePluginRequest)
pluginsRoute.HandleFunc("/public/{public_file:.*}", ch.ServePluginPublicRequest)
pluginsRoute.HandleFunc("/{anything:.*}", ch.ServePluginRequest)
services[product.ChannelKey] = &channelsWrapper{
app: &App{ch: ch},
}
services[product.PostKey] = &postServiceWrapper{
app: &App{ch: ch},
}
services[product.PermissionsKey] = &permissionsServiceWrapper{
app: &App{ch: ch},
}
services[product.TeamKey] = &teamServiceWrapper{
app: &App{ch: ch},
}
services[product.BotKey] = &botServiceWrapper{
app: &App{ch: ch},
}
services[product.UserKey] = &App{ch: ch}
services[product.PreferencesKey] = &preferencesServiceWrapper{
app: &App{ch: ch},
}
services[product.CommandKey] = &App{ch: ch}
services[product.ThreadsKey] = &App{ch: ch}
return ch, nil
}
@@ -314,35 +215,22 @@ func (ch *Channels) RemoveConfigListener(id string) {
ch.cfgSvc.RemoveConfigListener(id)
}
func (ch *Channels) License() *model.License {
return ch.licenseSvc.GetLicense()
}
func (ch *Channels) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError {
return ch.licenseSvc.RequestTrialLicenseWithExtraFields(requesterID, trialRequest)
}
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
return ch.licenseSvc.RequestTrialLicense(requesterID, users, termsAccepted,
receiveEmailsAccepted)
}
func (ch *Channels) RunMultiHook(hookRunnerFunc func(hooks plugin.Hooks) bool, hookId int) {
if env := ch.GetPluginsEnvironment(); env != nil {
env.RunMultiPluginHook(hookRunnerFunc, hookId)
}
}
func (ch *Channels) HooksForPluginOrProduct(id string) (plugin.Hooks, error) {
var hooks plugin.Hooks
if env := ch.GetPluginsEnvironment(); env != nil {
// we intentionally ignore the error here, because the id can be a product id
// we are going to check if we have the hooks or not
hooks, _ = env.HooksForPlugin(id)
if hooks != nil {
return hooks, nil
}
func (ch *Channels) HooksForPlugin(id string) (plugin.Hooks, error) {
env := ch.GetPluginsEnvironment()
if env == nil {
return nil, errors.New("plugins are not initialized")
}
return nil, fmt.Errorf("could not find hooks for id %s", id)
hooks, err := env.HooksForPlugin(id)
if err != nil {
return nil, err
}
return hooks, nil
}

Просмотреть файл

@@ -13,27 +13,9 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
// Ensure cloud service wrapper implements `product.CloudService`
var _ product.CloudService = (*cloudWrapper)(nil)
// cloudWrapper provides an implementation of `product.CloudService` for use by products.
type cloudWrapper struct {
cloud einterfaces.CloudInterface
}
func (c *cloudWrapper) GetCloudLimits() (*model.ProductLimits, error) {
if c.cloud != nil {
return c.cloud.GetCloudLimits("")
}
return &model.ProductLimits{}, nil
}
func (a *App) getSysAdminsEmailRecipients() ([]*model.User, *model.AppError) {
userOptions := &model.UserGetOptions{
Page: 0,

Просмотреть файл

@@ -33,10 +33,6 @@ func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {
return
}
pluginID := msg.Props["PluginID"]
// if the plugin key is empty, the message might be coming from a product.
if pluginID == "" {
pluginID = msg.Props["ProductID"]
}
eventID := msg.Props["EventID"]
if pluginID == "" || eventID == "" {
s.Log().Warn("Invalid ClusterMessage.Props values for plugin event",
@@ -46,12 +42,7 @@ func (s *Server) clusterPluginEventHandler(msg *model.ClusterMessage) {
return
}
channels, ok := s.products["channels"].(*Channels)
if !ok {
return
}
hooks, err := channels.HooksForPluginOrProduct(pluginID)
hooks, err := s.Channels().HooksForPlugin(pluginID)
if err != nil {
s.Log().Warn("Getting hooks for plugin failed", mlog.String("plugin_id", pluginID), mlog.Err(err))
return

Просмотреть файл

@@ -29,7 +29,6 @@ import (
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/platform/services/docextractor"
@@ -49,18 +48,6 @@ const (
maxContentExtractionSize = 1024 * 1024 // 1MB
)
// Ensure fileInfo service wrapper implements `product.FileInfoStoreService`
var _ product.FileInfoStoreService = (*fileInfoWrapper)(nil)
// fileInfoWrapper implements `product.FileInfoStoreService` for use by products.
type fileInfoWrapper struct {
srv *Server
}
func (f *fileInfoWrapper) GetFileInfo(fileID string) (*model.FileInfo, *model.AppError) {
return f.srv.getFileInfo(fileID)
}
func (a *App) FileBackend() filestore.FileBackend {
return a.ch.filestore
}

Просмотреть файл

@@ -191,8 +191,7 @@ func SetupWithoutPreloadMigrations(tb testing.TB) *TestHelper {
func SetupWithStoreMock(tb testing.TB) *TestHelper {
mockStore := testlib.GetMockStoreForSetupFunctions()
setupOptions := []Option{SkipProductsInitialization()}
th := setupTestHelper(mockStore, false, false, nil, setupOptions, tb)
th := setupTestHelper(mockStore, false, false, nil, nil, tb)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
@@ -213,8 +212,7 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper {
func SetupEnterpriseWithStoreMock(tb testing.TB) *TestHelper {
mockStore := testlib.GetMockStoreForSetupFunctions()
setupOptions := []Option{SkipProductsInitialization()}
th := setupTestHelper(mockStore, true, false, nil, setupOptions, tb)
th := setupTestHelper(mockStore, true, false, nil, nil, tb)
statusMock := mocks.StatusStore{}
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)

Просмотреть файл

@@ -11,7 +11,6 @@ import (
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
@@ -19,29 +18,16 @@ const (
JWTDefaultTokenExpiration = 7 * 24 * time.Hour // 7 days of expiration
)
// ensure the license service wrapper implements `product.LicenseService`
var _ product.LicenseService = (*licenseWrapper)(nil)
// licenseWrapper is an adapter struct that only exposes the
// config related functionality to be passed down to other products.
type licenseWrapper struct {
srv *Server
func (ch *Channels) License() *model.License {
return ch.srv.License()
}
func (w *licenseWrapper) Name() product.ServiceKey {
return product.LicenseKey
}
func (w *licenseWrapper) GetLicense() *model.License {
return w.srv.License()
}
func (w *licenseWrapper) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError {
if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
func (ch *Channels) RequestTrialLicenseWithExtraFields(requesterID string, trialRequest *model.TrialLicenseRequest) *model.AppError {
if *ch.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
requester, err := w.srv.userService.GetUser(requesterID)
requester, err := ch.srv.userService.GetUser(requesterID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
@@ -52,17 +38,17 @@ func (w *licenseWrapper) RequestTrialLicenseWithExtraFields(requesterID string,
}
}
if w.srv.Cloud.ValidateBusinessEmail(requesterID, trialRequest.ContactEmail) != nil {
if ch.srv.Cloud.ValidateBusinessEmail(requesterID, trialRequest.ContactEmail) != nil {
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request.business-email", nil, "", http.StatusBadRequest)
}
// Create a new struct only using the fields from the request that are allowed to be set by the client
sanitizedRequest := &model.TrialLicenseRequest{
ServerID: w.srv.TelemetryId(),
ServerID: ch.srv.TelemetryId(),
Name: requester.GetDisplayName(model.ShowFullName),
Email: requester.Email,
SiteName: *w.srv.platform.Config().TeamSettings.SiteName,
SiteURL: *w.srv.platform.Config().ServiceSettings.SiteURL,
SiteName: *ch.srv.platform.Config().TeamSettings.SiteName,
SiteURL: *ch.srv.platform.Config().ServiceSettings.SiteURL,
Users: trialRequest.Users,
TermsAccepted: trialRequest.TermsAccepted,
ReceiveEmailsAccepted: trialRequest.ReceiveEmailsAccepted,
@@ -77,12 +63,12 @@ func (w *licenseWrapper) RequestTrialLicenseWithExtraFields(requesterID string,
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
}
return w.srv.platform.RequestTrialLicense(sanitizedRequest)
return ch.srv.platform.RequestTrialLicense(sanitizedRequest)
}
// DEPRECATED - use RequestTrialLicenseWithExtraFields instead. This function remains to support the Plugin API.
func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
if *w.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
// Deprecated: Use RequestTrialLicenseWithExtraFields instead. This function remains to support the Plugin API.
func (ch *Channels) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError {
if *ch.srv.platform.Config().ExperimentalSettings.RestrictSystemAdmin {
return model.NewAppError("RequestTrialLicense", "api.restricted_system_admin", nil, "", http.StatusForbidden)
}
@@ -94,7 +80,7 @@ func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, term
return model.NewAppError("RequestTrialLicense", "api.license.request-trial.bad-request", nil, "", http.StatusBadRequest)
}
requester, err := w.srv.userService.GetUser(requesterID)
requester, err := ch.srv.userService.GetUser(requesterID)
if err != nil {
var nfErr *store.ErrNotFound
switch {
@@ -106,17 +92,17 @@ func (w *licenseWrapper) RequestTrialLicense(requesterID string, users int, term
}
trialLicenseRequest := &model.TrialLicenseRequest{
ServerID: w.srv.TelemetryId(),
ServerID: ch.srv.TelemetryId(),
Name: requester.GetDisplayName(model.ShowFullName),
Email: requester.Email,
SiteName: *w.srv.platform.Config().TeamSettings.SiteName,
SiteURL: *w.srv.platform.Config().ServiceSettings.SiteURL,
SiteName: *ch.srv.platform.Config().TeamSettings.SiteName,
SiteURL: *ch.srv.platform.Config().ServiceSettings.SiteURL,
Users: users,
TermsAccepted: termsAccepted,
ReceiveEmailsAccepted: receiveEmailsAccepted,
}
return w.srv.platform.RequestTrialLicense(trialLicenseRequest)
return ch.srv.platform.RequestTrialLicense(trialLicenseRequest)
}
// JWTClaims custom JWT claims with the needed information for the

Просмотреть файл

@@ -20,7 +20,6 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/mattermost/mattermost/server/v8/channels/testlib"
"github.com/mattermost/mattermost/server/v8/config"
@@ -1519,8 +1518,7 @@ func TestPushNotificationRace(t *testing.T) {
Return(&model.Preference{Value: "test"}, nil)
mockStore.On("Preference").Return(&mockPreferenceStore)
s := &Server{
products: make(map[string]product.Product),
Router: mux.NewRouter(),
Router: mux.NewRouter(),
}
var err error
s.platform, err = platform.New(
@@ -1531,16 +1529,9 @@ func TestPushNotificationRace(t *testing.T) {
platform.SetExportFileStore(&fmocks.FileBackend{}),
platform.StoreOverride(mockStore))
require.NoError(t, err)
serviceMap := map[product.ServiceKey]any{
ServerKey: s,
product.ConfigKey: s.platform,
product.LicenseKey: &licenseWrapper{s},
product.FilestoreKey: s.FileBackend(),
product.ExportFilestoreKey: s.ExportFileBackend(),
}
ch, err := NewChannels(serviceMap)
ch, err := NewChannels(s)
require.NoError(t, err)
s.products["channels"] = ch
s.ch = ch
app := New(ServerConnector(s.Channels()))
require.NotPanics(t, func() {

Просмотреть файл

@@ -69,7 +69,7 @@ func (a *App) CompleteOnboarding(c request.CTX, request *model.CompleteOnboardin
return
}
hooks, err := a.ch.HooksForPluginOrProduct(id)
hooks, err := a.ch.HooksForPlugin(id)
if err != nil {
c.Logger().Warn("Getting hooks for plugin failed", mlog.String("plugin_id", id), mlog.Err(err))
return

Просмотреть файл

@@ -4122,7 +4122,7 @@ func (a *OpenTracingAppLayer) EnableUserAccessToken(c request.CTX, token *model.
return resultVar0
}
func (a *OpenTracingAppLayer) EnsureBot(rctx request.CTX, productID string, bot *model.Bot) (string, error) {
func (a *OpenTracingAppLayer) EnsureBot(rctx request.CTX, pluginID string, bot *model.Bot) (string, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.EnsureBot")
@@ -4134,7 +4134,7 @@ func (a *OpenTracingAppLayer) EnsureBot(rctx request.CTX, productID string, bot
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.EnsureBot(rctx, productID, bot)
resultVar0, resultVar1 := a.app.EnsureBot(rctx, pluginID, bot)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))

Просмотреть файл

@@ -103,17 +103,6 @@ func SkipPostInitialization() Option {
}
}
// SkipProductsInitialization is intended for testing only, in cases
// where we're mocking components like the store and products cannot
// be initialized correctly
func SkipProductsInitialization() Option {
return func(s *Server) error {
s.skipProductsInit = true
return nil
}
}
type AppOption func(a *App)
type AppOptionCreator func() []AppOption

Просмотреть файл

@@ -14,37 +14,11 @@ import (
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/product"
)
const permissionsExportBatchSize = 100
const systemSchemeName = "00000000-0000-0000-0000-000000000000" // Prevents collisions with user-created schemes.
// Ensure permissions service wrapper implements `product.PermissionService`
var _ product.PermissionService = (*permissionsServiceWrapper)(nil)
// permissionsServiceWrapper provides an implementation of `product.PermissionService` for use by products.
type permissionsServiceWrapper struct {
app AppIface
}
func (s *permissionsServiceWrapper) HasPermissionTo(userID string, permission *model.Permission) bool {
return s.app.HasPermissionTo(userID, permission)
}
func (s *permissionsServiceWrapper) HasPermissionToTeam(c request.CTX, userID string, teamID string, permission *model.Permission) bool {
return s.app.HasPermissionToTeam(c, userID, teamID, permission)
}
func (s *permissionsServiceWrapper) HasPermissionToChannel(c request.CTX, askingUserID string, channelID string, permission *model.Permission) bool {
return s.app.HasPermissionToChannel(c, askingUserID, channelID, permission)
}
func (s *permissionsServiceWrapper) RolesGrantPermission(roleNames []string, permissionId string) bool {
return s.app.RolesGrantPermission(roleNames, permissionId)
}
func (a *App) ResetPermissionsSystem() *model.AppError {
// Reset all Teams to not have a scheme.
if err := a.Srv().Store().Team().ResetAllTeamSchemes(); err != nil {

Просмотреть файл

@@ -10,17 +10,10 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
// ensure cluster service wrapper implements `product.ClusterService`
var _ product.ClusterService = (*PlatformService)(nil)
// Ensure KV store wrapper implements `product.KVStoreService`
var _ product.KVStoreService = (*PlatformService)(nil)
func (ps *PlatformService) Cluster() einterfaces.ClusterInterface {
return ps.clusterIFace
}
@@ -57,8 +50,7 @@ func (ps *PlatformService) PublishPluginClusterEvent(productID string, ev model.
SendType: opts.SendType,
WaitForAllToSend: false,
Props: map[string]string{
"ProductID": productID,
"EventID": ev.Id,
"EventID": ev.Id,
},
Data: ev.Data,
}

Просмотреть файл

@@ -21,7 +21,6 @@ import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/utils"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/config"
"github.com/mattermost/mattermost/server/v8/einterfaces"
@@ -37,9 +36,6 @@ type ServiceConfig struct {
Cluster einterfaces.ClusterInterface
}
// ensure the config wrapper implements `product.ConfigService`
var _ product.ConfigService = (*PlatformService)(nil)
func (ps *PlatformService) Config() *model.Config {
return ps.configStore.Get()
}

Просмотреть файл

@@ -17,7 +17,6 @@ import (
"sync"
"github.com/blang/semver/v4"
"github.com/gorilla/mux"
svg "github.com/h2non/go-is-svg"
"github.com/pkg/errors"
@@ -25,7 +24,6 @@ import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
"github.com/mattermost/mattermost/server/v8/platform/services/marketplace"
)
@@ -41,31 +39,6 @@ type pluginSignaturePath struct {
signaturePath string
}
// Ensure routerService implements `product.RouterService`
var _ product.RouterService = (*routerService)(nil)
type routerService struct {
mu sync.Mutex
routerMap map[string]*mux.Router
}
func newRouterService() *routerService {
return &routerService{
routerMap: make(map[string]*mux.Router),
}
}
func (rs *routerService) RegisterRouter(productID string, sub *mux.Router) {
rs.mu.Lock()
defer rs.mu.Unlock()
rs.routerMap[productID] = sub
}
func (rs *routerService) getHandler(productID string) (http.Handler, bool) {
handler, ok := rs.routerMap[productID]
return handler, ok
}
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//

Просмотреть файл

@@ -1302,7 +1302,7 @@ func TestHookReactionHasBeenRemoved(t *testing.T) {
}
func TestHookRunDataRetention(t *testing.T) {
th := Setup(t, SkipProductsInitialization()).InitBasic()
th := Setup(t).InitBasic()
defer th.TearDown()
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,

Просмотреть файл

@@ -21,12 +21,7 @@ import (
func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
if handler, ok := ch.routerSvc.getHandler(params["plugin_id"]); ok {
ch.servePluginRequest(w, r, func(*plugin.Context, http.ResponseWriter, *http.Request) {
handler.ServeHTTP(w, r)
})
return
}
pluginID := params["plugin_id"]
pluginsEnvironment := ch.GetPluginsEnvironment()
if pluginsEnvironment == nil {
@@ -38,10 +33,10 @@ func (ch *Channels) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
return
}
hooks, err := pluginsEnvironment.HooksForPlugin(params["plugin_id"])
hooks, err := pluginsEnvironment.HooksForPlugin(pluginID)
if err != nil {
mlog.Debug("Access to route for non-existent plugin",
mlog.String("missing_plugin_id", params["plugin_id"]),
mlog.String("missing_plugin_id", pluginID),
mlog.String("url", r.URL.String()),
mlog.Err(err))
http.NotFound(w, r)

Просмотреть файл

@@ -82,7 +82,7 @@ func (ch *Channels) verifyPlugin(plugin, signature io.ReadSeeker) *model.AppErro
if err := verifySignature(bytes.NewReader(mattermostPluginPublicKey), plugin, signature); err == nil {
return nil
}
publicKeys := ch.cfgSvc.Config().PluginSettings.SignaturePublicKeyFiles
publicKeys := ch.srv.Config().PluginSettings.SignaturePublicKeyFiles
for _, pk := range publicKeys {
pkBytes, appErr := ch.srv.getPublicKey(pk)
if appErr != nil {

Просмотреть файл

@@ -20,7 +20,6 @@ import (
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
"github.com/mattermost/mattermost/server/v8/platform/services/cache"
@@ -34,38 +33,6 @@ const (
var atMentionPattern = regexp.MustCompile(`\B@`)
// Ensure post service wrapper implements `product.PostService`
var _ product.PostService = (*postServiceWrapper)(nil)
// postServiceWrapper provides an implementation of `product.PostService` for use by products.
type postServiceWrapper struct {
app AppIface
}
func (s *postServiceWrapper) CreatePost(ctx request.CTX, post *model.Post) (*model.Post, *model.AppError) {
return s.app.CreatePostMissingChannel(ctx, post, true, true)
}
func (s *postServiceWrapper) GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError) {
return s.app.GetPostsByIds(postIDs)
}
func (s *postServiceWrapper) SendEphemeralPost(ctx request.CTX, userID string, post *model.Post) *model.Post {
return s.app.SendEphemeralPost(ctx, userID, post)
}
func (s *postServiceWrapper) GetPost(postID string) (*model.Post, *model.AppError) {
return s.app.GetSinglePost(postID, false)
}
func (s *postServiceWrapper) DeletePost(ctx request.CTX, postID, productID string) (*model.Post, *model.AppError) {
return s.app.DeletePost(ctx, postID, productID)
}
func (s *postServiceWrapper) UpdatePost(ctx request.CTX, post *model.Post, safeUpdate bool) (*model.Post, *model.AppError) {
return s.app.UpdatePost(ctx, post, false)
}
func (a *App) CreatePostAsUser(c request.CTX, post *model.Post, currentSessionId string, setOnline bool) (*model.Post, *model.AppError) {
// Check that channel has not been deleted
channel, errCh := a.Srv().Store().Channel().Get(post.ChannelId, true)

Просмотреть файл

@@ -11,29 +11,8 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/product"
)
// Ensure preferences service wrapper implements `product.PreferencesService`
var _ product.PreferencesService = (*preferencesServiceWrapper)(nil)
// preferencesServiceWrapper provides an implementation of `product.PreferencesService` for use by products.
type preferencesServiceWrapper struct {
app AppIface
}
func (w *preferencesServiceWrapper) GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError) {
return w.app.GetPreferencesForUser(c, userID)
}
func (w *preferencesServiceWrapper) UpdatePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError {
return w.app.UpdatePreferences(c, userID, preferences)
}
func (w *preferencesServiceWrapper) DeletePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError {
return w.app.DeletePreferences(c, userID, preferences)
}
func (a *App) GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError) {
preferences, err := a.Srv().Store().Preference().GetAll(userID)
if err != nil {

Просмотреть файл

@@ -1,79 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"fmt"
"strings"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/product"
)
func (s *Server) initializeProducts(
productMap map[string]product.Manifest,
serviceMap map[product.ServiceKey]any,
) error {
// create a product map to consume
pmap := make(map[string]struct{})
for name := range productMap {
if !s.shouldStart(name) {
continue
}
pmap[name] = struct{}{}
}
// We figure out the initialization order by trial and error fashion hence maxTry
// is the 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.
maxTry := len(pmap) * len(pmap)
for len(pmap) > 0 && maxTry != 0 {
initLoop:
for product := range pmap {
manifest := productMap[product]
// we have dependencies defined. Here we check if the serviceMap
// has all the dependencies registered. If not, we continue to the
// loop to let other products initialize and register their services
// if they have any.
for key := range manifest.Dependencies {
if _, ok := serviceMap[key]; !ok {
maxTry--
continue initLoop
}
}
// some products can register themselves/their services
initializer := manifest.Initializer
prod, err := initializer(serviceMap)
if err != nil {
return fmt.Errorf("error initializing product %q: %w", product, err)
}
s.products[product] = prod
// we remove this product from the map to not try to initialize it again
delete(pmap, product)
}
}
if maxTry == 0 && len(pmap) != 0 {
var products string
for p := range pmap {
products = strings.Join([]string{products, fmt.Sprintf("%q", p)}, " ")
}
return fmt.Errorf("could not initialize product(s) due to circular dependency: %s", products)
}
return nil
}
func (s *Server) shouldStart(product string) bool {
if s.skipProductsInit && product != "channels" {
s.Log().Warn("Skipping product start: disabled via server options", mlog.String("product", product))
return false
}
return true
}

Просмотреть файл

@@ -1,164 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/config"
)
const (
testSrvKey1 = "test_1"
testSrvKey2 = "test_2"
)
type productA struct{}
func newProductA(m map[product.ServiceKey]any) (product.Product, error) {
m[testSrvKey1] = nil
return &productA{}, nil
}
func (p *productA) Start() error { return nil }
func (p *productA) Stop() error { return nil }
type productB struct{}
func newProductB(m map[product.ServiceKey]any) (product.Product, error) {
m[testSrvKey2] = nil
return &productB{}, nil
}
func (p *productB) Start() error { return nil }
func (p *productB) Stop() error { return nil }
func TestInitializeProducts(t *testing.T) {
configStore := config.NewTestMemoryStore()
memoryConfig := configStore.Get()
memoryConfig.SqlSettings = *mainHelper.GetSQLSettings()
configStore.Set(memoryConfig)
ps, err := platform.New(platform.ServiceConfig{ConfigStore: configStore})
require.NoError(t, err)
defer ps.Shutdown()
t.Run("2 products and no circular dependency", func(t *testing.T) {
serviceMap := map[product.ServiceKey]any{
product.ConfigKey: nil,
product.LicenseKey: nil,
product.FilestoreKey: nil,
product.ExportFilestoreKey: nil,
product.ClusterKey: nil,
}
products := map[string]product.Manifest{
"productA": {
Initializer: newProductA,
Dependencies: map[product.ServiceKey]struct{}{
product.ConfigKey: {},
product.LicenseKey: {},
product.FilestoreKey: {},
product.ExportFilestoreKey: {},
product.ClusterKey: {},
},
},
"productB": {
Initializer: newProductB,
Dependencies: map[product.ServiceKey]struct{}{
product.ConfigKey: {},
testSrvKey1: {},
product.FilestoreKey: {},
product.ExportFilestoreKey: {},
product.ClusterKey: {},
},
},
}
server := &Server{
products: make(map[string]product.Product),
platform: ps,
}
err = server.initializeProducts(products, serviceMap)
require.NoError(t, err)
require.Len(t, server.products, 2)
})
t.Run("2 products and circular dependency", func(t *testing.T) {
serviceMap := map[product.ServiceKey]any{
product.ConfigKey: nil,
product.LicenseKey: nil,
product.FilestoreKey: nil,
product.ExportFilestoreKey: nil,
product.ClusterKey: nil,
}
products := map[string]product.Manifest{
"productA": {
Initializer: newProductA,
Dependencies: map[product.ServiceKey]struct{}{
product.ConfigKey: {},
product.LicenseKey: {},
product.FilestoreKey: {},
product.ExportFilestoreKey: {},
product.ClusterKey: {},
testSrvKey2: {},
},
},
"productB": {
Initializer: newProductB,
Dependencies: map[product.ServiceKey]struct{}{
product.ConfigKey: {},
testSrvKey1: {},
product.FilestoreKey: {},
product.ExportFilestoreKey: {},
product.ClusterKey: {},
},
},
}
server := &Server{
products: make(map[string]product.Product),
platform: ps,
}
err := server.initializeProducts(products, serviceMap)
require.Error(t, err)
})
t.Run("2 products and one w/o any dependency", func(t *testing.T) {
serviceMap := map[product.ServiceKey]any{
product.ConfigKey: nil,
product.LicenseKey: nil,
product.FilestoreKey: nil,
product.ExportFilestoreKey: nil,
product.ClusterKey: nil,
}
products := map[string]product.Manifest{
"productA": {
Initializer: newProductA,
Dependencies: map[product.ServiceKey]struct{}{
product.ConfigKey: {},
product.LicenseKey: {},
},
},
"productB": {
Initializer: newProductB,
},
}
server := &Server{
products: make(map[string]product.Product),
platform: ps,
}
err := server.initializeProducts(products, serviceMap)
require.NoError(t, err)
require.Len(t, server.products, 2)
})
}

Просмотреть файл

@@ -60,7 +60,6 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/jobs/refresh_post_stats"
"github.com/mattermost/mattermost/server/v8/channels/jobs/resend_invitation_email"
"github.com/mattermost/mattermost/server/v8/channels/jobs/s3_path_migration"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/utils"
"github.com/mattermost/mattermost/server/v8/config"
@@ -117,8 +116,6 @@ type Server struct {
runEssentialJobs bool
Jobs *jobs.JobServer
licenseWrapper *licenseWrapper
timezones *timezones.Timezones
htmlTemplateWatcher *templates.Container
@@ -141,8 +138,7 @@ type Server struct {
Audit *audit.Audit
joinCluster bool
// startSearchEngine bool
joinCluster bool
skipPostInit bool
Cloud einterfaces.CloudInterface
@@ -151,10 +147,7 @@ type Server struct {
tracer *tracing.Tracer
skipProductsInit bool
products map[string]product.Product
services map[product.ServiceKey]any
ch *Channels
}
func (s *Server) Store() store.Store {
@@ -179,8 +172,6 @@ func NewServer(options ...Option) (*Server, error) {
RootRouter: rootRouter,
LocalRouter: localRouter,
timezones: timezones.New(),
products: make(map[string]product.Product),
services: make(map[product.ServiceKey]any),
}
for _, option := range options {
@@ -228,10 +219,6 @@ func NewServer(options ...Option) (*Server, error) {
return nil, errors.Wrapf(err, "unable to create users service")
}
s.licenseWrapper = &licenseWrapper{
srv: s,
}
s.teamService, err = teams.New(teams.ServiceConfig{
TeamStore: s.Store().Team(),
ChannelStore: s.Store().Channel(),
@@ -245,29 +232,6 @@ func NewServer(options ...Option) (*Server, error) {
return nil, errors.Wrapf(err, "unable to create teams service")
}
// ensure app implements `product.UserService`
var _ product.UserService = (*App)(nil)
app := New(ServerConnector(s.Channels()))
serviceMap := map[product.ServiceKey]any{
ServerKey: s,
product.ConfigKey: s.platform,
product.LicenseKey: s.licenseWrapper,
product.FilestoreKey: s.platform.FileBackend(),
product.ExportFilestoreKey: s.platform.ExportFileBackend(),
product.FileInfoStoreKey: &fileInfoWrapper{srv: s},
product.ClusterKey: s.platform,
product.UserKey: app,
product.LogKey: s.platform.Log(),
product.CloudKey: &cloudWrapper{cloud: s.Cloud},
product.KVStoreKey: s.platform,
product.StoreKey: store.NewStoreServiceAdapter(s.Store()),
product.SystemKey: &systemServiceAdapter{server: s},
product.SessionKey: app,
product.FrontendKey: app,
product.CommandKey: app,
}
// It is important to initialize the hub only after the global logger is set
// to avoid race conditions while logging from inside the hub.
// Step 4: Start platform
@@ -276,21 +240,17 @@ func NewServer(options ...Option) (*Server, error) {
// NOTE: There should be no call to App.Srv().Channels() before step 5 is done
// otherwise it will throw a panic.
// Step 5: Initialize products.
// Step 5: Initialize channels.
// Depends on s.httpService, and depends on the hub to be initialized.
// Otherwise we run into race conditions.
err = s.initializeProducts(product.GetProducts(), serviceMap)
channels, err := NewChannels(s)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize products")
return nil, errors.Wrap(err, "failed to initialize channels")
}
s.services = serviceMap
s.ch = channels
// After channel is initialized set it to the App object
channelsWrapper, ok := serviceMap[product.ChannelKey].(*channelsWrapper)
if !ok {
return nil, errors.Wrap(err, "channels product is not initialized")
}
app.ch = channelsWrapper.app.ch
app := New(ServerConnector(channels))
// -------------------------------------------------------------------------
// Everything below this is not order sensitive and safe to be moved around.
@@ -596,8 +556,7 @@ func (s *Server) AppOptions() []AppOption {
}
func (s *Server) Channels() *Channels {
ch, _ := s.products["channels"].(*Channels)
return ch
return s.ch
}
// Return Database type (postgres or mysql) and current version of the schema
@@ -770,13 +729,11 @@ func (s *Server) Shutdown() {
}
}
// Stop products.
// This needs to happen last because products are dependent
// Stop channels.
// This needs to happen last because channels are dependent
// on parent services.
for name, product := range s.products {
if err2 := product.Stop(); err2 != nil {
s.Log().Warn("Unable to cleanly stop product", mlog.String("name", name), mlog.Err(err2))
}
if err = s.Channels().Stop(); err != nil {
s.Log().Warn("Unable to cleanly stop channels", mlog.Err(err))
}
if err = s.platform.Shutdown(); err != nil {
@@ -874,21 +831,11 @@ 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 {
// Start channels.
// This needs to happen before because channels is dependent on the HTTP server.
if err := s.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)
}
}
if s.joinCluster && s.platform.Cluster() != nil {
s.registerClusterHandlers()

Просмотреть файл

@@ -172,7 +172,7 @@ func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "postgres")
th := Setup(t, SkipProductsInitialization())
th := Setup(t)
defer th.TearDown()
databaseType, mattermostVersion := th.Server.DatabaseTypeAndSchemaVersion()
@@ -181,7 +181,7 @@ func TestDatabaseTypeAndMattermostVersion(t *testing.T) {
os.Setenv("MM_SQLSETTINGS_DRIVERNAME", "mysql")
th2 := Setup(t, SkipProductsInitialization())
th2 := Setup(t)
defer th2.TearDown()
databaseType, mattermostVersion = th2.Server.DatabaseTypeAndSchemaVersion()

Просмотреть файл

@@ -26,40 +26,10 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/app/imaging"
"github.com/mattermost/mattermost/server/v8/channels/app/teams"
"github.com/mattermost/mattermost/server/v8/channels/app/users"
"github.com/mattermost/mattermost/server/v8/channels/product"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore"
)
// teamServiceWrapper provides an implementation of `product.TeamService` to be used by products.
type teamServiceWrapper struct {
app AppIface
}
func (w *teamServiceWrapper) GetMember(c request.CTX, teamID, userID string) (*model.TeamMember, *model.AppError) {
return w.app.GetTeamMember(c, teamID, userID)
}
func (w *teamServiceWrapper) CreateMember(ctx request.CTX, teamID, userID string) (*model.TeamMember, *model.AppError) {
return w.app.AddTeamMember(ctx, teamID, userID)
}
func (w *teamServiceWrapper) GetGroup(groupID string) (*model.Group, *model.AppError) {
return w.app.GetGroup(groupID, nil, nil)
}
func (w *teamServiceWrapper) GetTeam(teamID string) (*model.Team, *model.AppError) {
return w.app.GetTeam(teamID)
}
func (w *teamServiceWrapper) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) {
users, _, err := w.app.GetGroupMemberUsersPage(groupID, page, perPage, nil)
return users, err
}
// Ensure the wrapper implements the product service.
var _ product.TeamService = (*teamServiceWrapper)(nil)
func (a *App) AdjustTeamsFromProductLimits(teamLimits *model.TeamsLimits) *model.AppError {
maxActiveTeams := *teamLimits.Active
teams, appErr := a.GetAllTeams()

Просмотреть файл

@@ -1,114 +0,0 @@
# 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/server/v8/channels/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.

Просмотреть файл

@@ -1,234 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
import (
"database/sql"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
)
// 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.CTX, post *model.Post) (*model.Post, *model.AppError)
GetPostsByIds(postIDs []string) ([]*model.Post, int64, *model.AppError)
SendEphemeralPost(ctx request.CTX, userID string, post *model.Post) *model.Post
GetPost(postID string) (*model.Post, *model.AppError)
DeletePost(ctx request.CTX, postID, productID string) (*model.Post, *model.AppError)
UpdatePost(c request.CTX, post *model.Post, safeUpdate bool) (*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 {
HasPermissionTo(userID string, permission *model.Permission) bool
HasPermissionToTeam(c request.CTX, userID, teamID string, permission *model.Permission) bool
HasPermissionToChannel(c request.CTX, askingUserID string, channelID string, permission *model.Permission) bool
RolesGrantPermission(roleNames []string, permissionID string) 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]any, broadcast *model.WebsocketBroadcast)
}
// 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)
GetDirectChannelOrCreate(userID1, userID2 string) (*model.Channel, *model.AppError)
GetChannelByID(channelID string) (*model.Channel, *model.AppError)
GetChannelMember(channelID string, userID string) (*model.ChannelMember, *model.AppError)
GetChannelsForTeamForUser(teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError)
GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError)
GetChannelMembers(channelID string, page, perPage int) (model.ChannelMembers, *model.AppError)
CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError)
UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError)
CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
AddUserToChannel(channelID, userID, asUserID string) (*model.ChannelMember, *model.AppError)
UpdateChannelMemberRoles(channelID, userID, newRoles string) (*model.ChannelMember, *model.AppError)
DeleteChannelMember(channelID, userID string) *model.AppError
AddChannelMember(channelID, 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(c request.CTX, user *model.User, sendNotifications bool) (*model.User, *model.AppError)
GetUserByEmail(email string) (*model.User, *model.AppError)
GetUserByUsername(username string) (*model.User, *model.AppError)
GetUsersFromProfiles(options *model.UserGetOptions) ([]*model.User, *model.AppError)
}
// TeamService provides team related utilities.
//
// The service shall be registered via app.TeamKey service key.
type TeamService interface {
GetMember(c request.CTX, teamID, userID string) (*model.TeamMember, *model.AppError)
CreateMember(ctx request.CTX, teamID, userID string) (*model.TeamMember, *model.AppError)
GetGroup(groupId string) (*model.Group, *model.AppError)
GetTeam(teamID string) (*model.Team, *model.AppError)
GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError)
}
// 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.CTX, productID string, bot *model.Bot) (string, error)
}
// ConfigService shall be registered via app.ConfigKey service key.
type ConfigService interface {
Config() *model.Config
AddConfigListener(listener func(*model.Config, *model.Config)) string
RemoveConfigListener(id string)
UpdateConfig(f func(*model.Config))
SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bool) (*model.Config, *model.Config, *model.AppError)
}
// 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 accessed after the channels product initialized.
//
// The service shall be registered via app.HooksKey service key.
type HooksService interface {
// RegisterHook checks whether if the 'hooks' implements any method of plugin.Hooks methods. Rather than
// using the whole plugin.Hooks interface with its 20+ methods, a product can implement any exiting method
// of plugin.Hooks w/o requiring to declare which method they implemented or not. This is going to be
// checked on runtime. We have individual interfaces for each method declared in plugin.Hooks interface.
// Hence, while registering a product, the service will check if the product implements any of these individual
// interfaces. If so, a map of hook IDs that are implemented will be used to call the hooks. The method will
// return an error in case if there is an incorrect implementation of the any of the individual interface in runtime.
// Consider checking plugin.Hooks for the reference.
// Following methods are not allowed to be implemented in the product:
// - plugin.Hooks.OnActivate
// - plugin.Hooks.OnDeactivate
// - plugin.Hooks.Implemented
// - plugin.Hooks.ServeHTTP
RegisterHooks(productID string, hooks any) error
}
// FilestoreService is the API for accessing the file store.
//
// The service shall be registered via app.FilestoreKey service key.
type FilestoreService interface {
filestore.FileBackend
}
// FileInfoStoreService is the API for accessing the file info store.
//
// The service shall be registered via app.FileInfoStoreKey service key.
type FileInfoStoreService interface {
GetFileInfo(fileID string) (*model.FileInfo, *model.AppError)
}
// CloudService is the API for accessing the cloud service APIs.
//
// The service shall be registered via app.CloudKey service key.
type CloudService interface {
GetCloudLimits() (*model.ProductLimits, error)
}
// KVStoreService is the API for accessing the KVStore service APIs.
//
// The service shall be registered via app.KVStoreKey service key.
type KVStoreService interface {
SetPluginKeyWithOptions(pluginID string, key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError)
KVGet(productID, key string) ([]byte, *model.AppError)
KVDelete(productID, key string) *model.AppError
KVList(productID string, page, perPage int) ([]string, *model.AppError)
}
// LogService is the API for accessing the log service APIs.
//
// The service shall be registered via app.LogKey service key.
type LogService interface {
mlog.LoggerIFace
}
// StoreService is the API for accessing the Store service APIs.
//
// The service shall be registered via app.StoreKey service key.
type StoreService interface {
GetMasterDB() *sql.DB
}
// SystemService is the API for accessing the System service APIs.
//
// The service shall be registered via app.SystemKey service key.
type SystemService interface {
GetDiagnosticId() string
}
// PreferencesService is the API for accessing the Preferences service APIs.
//
// The service shall be registered via app.PreferencesKey service key.
type PreferencesService interface {
GetPreferencesForUser(c request.CTX, userID string) (model.Preferences, *model.AppError)
UpdatePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError
DeletePreferencesForUser(c request.CTX, userID string, preferences model.Preferences) *model.AppError
}
// SessionService is the API for accessing the session.
//
// The service shall be registered via app.SessionKey service key.
type SessionService interface {
GetSessionById(sessionID string) (*model.Session, *model.AppError)
}
// FrontendService is the API for interacting with front end.
//
// The service shall be registered via app.FrontendKey service key.
type FrontendService interface {
OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError
}
// ThreadsService is the API for interacting with threads anywhere.
//
// The service shall be registered via app.ThreadsKey service key.
type ThreadsService interface {
RegisterCollectionAndTopic(productID string, collectionType, topicType string) error
}

Просмотреть файл

@@ -1,10 +0,0 @@
// 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

Просмотреть файл

@@ -1,24 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
type Product interface {
Start() error
Stop() error
}
type Manifest struct {
Initializer func(map[ServiceKey]any) (Product, error)
Dependencies map[ServiceKey]struct{}
}
var products = make(map[string]Manifest)
func RegisterProduct(name string, m Manifest) {
products[name] = m
}
func GetProducts() map[string]Manifest {
return products
}

Просмотреть файл

@@ -1,32 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package product
type ServiceKey string
const (
ChannelKey ServiceKey = "channel"
ConfigKey ServiceKey = "config"
LicenseKey ServiceKey = "license"
FilestoreKey ServiceKey = "filestore"
ExportFilestoreKey ServiceKey = "exportfilestore"
FileInfoStoreKey ServiceKey = "fileinfostore"
ClusterKey ServiceKey = "cluster"
CloudKey ServiceKey = "cloud"
PostKey ServiceKey = "post"
TeamKey ServiceKey = "team"
UserKey ServiceKey = "user"
PermissionsKey ServiceKey = "permissions"
RouterKey ServiceKey = "router"
BotKey ServiceKey = "bot"
LogKey ServiceKey = "log"
KVStoreKey ServiceKey = "kvstore"
StoreKey ServiceKey = "storekey"
SystemKey ServiceKey = "systemkey"
PreferencesKey ServiceKey = "preferenceskey"
SessionKey ServiceKey = "sessionkey"
FrontendKey ServiceKey = "frontendkey"
CommandKey ServiceKey = "commandkey"
ThreadsKey ServiceKey = "threadskey"
)

Просмотреть файл

@@ -13,7 +13,6 @@ import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/product"
)
type StoreResult[T any] struct {
@@ -74,7 +73,7 @@ type Store interface {
GetAppliedMigrations() ([]model.AppliedMigration, error)
GetDbVersion(numerical bool) (string, error)
// GetInternalMasterDB allows access to the raw master DB
// handle for the multi-product architecture.
// handle for plugins.
GetInternalMasterDB() *sql.DB
GetInternalReplicaDB() *sql.DB
TotalMasterDbConnections() int
@@ -1117,21 +1116,3 @@ type SidebarCategorySearchOpts struct {
ExcludeTeam bool
Type model.SidebarCategoryType
}
// Ensure store service adapter implements `product.StoreService`
var _ product.StoreService = (*StoreServiceAdapter)(nil)
// StoreServiceAdapter provides a simple Store wrapper for use with products.
type StoreServiceAdapter struct {
store Store
}
func NewStoreServiceAdapter(store Store) *StoreServiceAdapter {
return &StoreServiceAdapter{
store: store,
}
}
func (a *StoreServiceAdapter) GetMasterDB() *sql.DB {
return a.store.GetInternalMasterDB()
}

Просмотреть файл

@@ -55,7 +55,7 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper {
tb.SkipNow()
}
th := setupTestHelper(tb, false, []app.Option{app.SkipProductsInitialization()})
th := setupTestHelper(tb, false, nil)
emptyMockStore := mocks.Store{}
emptyMockStore.On("Close").Return(nil)
th.App.Srv().SetStore(&emptyMockStore)

Просмотреть файл

@@ -78,7 +78,6 @@ const (
TrackConfigExport = "config_export"
TrackConfigWrangler = "config_wrangler"
TrackFeatureFlags = "config_feature_flags"
TrackConfigProducts = "products"
TrackPermissionsGeneral = "permissions_general"
TrackPermissionsSystemScheme = "permissions_system_scheme"
TrackPermissionsTeamSchemes = "permissions_team_schemes"

Просмотреть файл

@@ -3494,7 +3494,7 @@ type Config struct {
DataRetentionSettings DataRetentionSettings
MessageExportSettings MessageExportSettings
JobSettings JobSettings
ProductSettings ProductSettings
ProductSettings ProductSettings // Deprecated: Remove in next major version:: https://mattermost.atlassian.net/browse/MM-56655
PluginSettings PluginSettings
DisplaySettings DisplaySettings
GuestAccountsSettings GuestAccountsSettings

Просмотреть файл

@@ -40,13 +40,6 @@ var excludedPluginHooks = []string{
"ServeMetrics",
}
var excludedProductHooks = []string{
"Implemented",
"OnActivate",
"OnDeactivate",
"ServeHTTP",
}
type IHookEntry struct {
FuncName string
Args *ast.FieldList
@@ -385,62 +378,6 @@ func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name
{{end}}
`
var productHooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make pluginapi"
// DO NOT EDIT
package plugin
{{range .HooksMethods}}
type {{.Name}}IFace interface {
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
}
{{end}}
type HooksAdapter struct {
implemented map[int]struct{}
productHooks any
}
func NewAdapter(productHooks any) (*HooksAdapter, error) {
a := &HooksAdapter{
implemented: make(map[int]struct{}),
productHooks: productHooks,
}
var tt reflect.Type
ft := reflect.TypeOf(productHooks)
{{range .HooksMethods}}
// Assessing the type of the productHooks if it individually implements {{.Name}} interface.
tt = reflect.TypeOf((*{{.Name}}IFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[{{.Name}}ID] = struct{}{}
} else if _, ok := ft.MethodByName("{{.Name}}"); ok{
return nil, errors.New("hook has {{.Name}} method but does not implement plugin.{{.Name}} interface")
}
{{end}}
return a, nil
}
{{range .HooksMethods}}
func (a *HooksAdapter) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
if _, ok := a.implemented[{{.Name}}ID]; !ok {
panic("product hooks must implement {{.Name}}")
}
{{if .Return}}return a.productHooks.({{.Name}}IFace).{{.Name}}({{valuesOnly .Params}}){{else}}a.productHooks.({{.Name}}IFace).{{.Name}}({{valuesOnly .Params}}){{end}}
}
{{end}}
`
var apiTimerLayerTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
@@ -586,39 +523,6 @@ func generateHooksGlue(info *PluginInterfaceInfo) {
}
}
func generateProductHooksInterfaces(info *PluginInterfaceInfo) {
templateFunctions := map[string]any{
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, false) },
}
templateParams := HooksTemplateParams{}
for _, hook := range info.Hooks {
templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
Name: hook.FuncName,
Params: hook.Args,
Return: hook.Results,
})
}
productHooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(productHooksTemplate)
if err != nil {
panic(err)
}
templateResult := &bytes.Buffer{}
productHooksTemplate.Execute(templateResult, &templateParams)
formatted, err := imports.Process("", templateResult.Bytes(), nil)
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), "product_hooks_generated.go"), formatted, 0664); err != nil {
panic(err)
}
}
func generatePluginTimerLayer(info *PluginInterfaceInfo) {
templateFunctions := map[string]any{
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
@@ -723,8 +627,6 @@ func main() {
if err != nil {
fmt.Println("Unable to get plugin info: " + err.Error())
}
log.Println("Generating product hooks interfaces")
generateProductHooksInterfaces(removeExcluded(forRPC, excludedProductHooks))
log.Println("Generating plugin hooks glue")
generateHooksGlue(removeExcluded(forRPC, excludedPluginHooks))

Просмотреть файл

@@ -1,41 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"net/http"
)
type RegisteredProduct struct {
ProductID string
Adapter Hooks
}
func (rp *RegisteredProduct) Implements(hookId int) bool {
adapter, ok := rp.Adapter.(*HooksAdapter)
if !ok {
return false
}
_, ok = adapter.implemented[hookId]
return ok
}
// Implemented method is overridden intentionally to prevent calling it from outside.
func (a *HooksAdapter) Implemented() ([]string, error) {
return nil, nil
}
// OnActivate is overridden intentionally as product should not call it.
func (a *HooksAdapter) OnActivate() error {
return nil
}
// OnDeactivate is overridden intentionally as product should not call it.
func (a *HooksAdapter) OnDeactivate() error {
return nil
}
// ServeHTTP is overridden intentionally as product should not call it.
func (a *HooksAdapter) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {}

Просмотреть файл

@@ -1,824 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Code generated by "make pluginapi"
// DO NOT EDIT
package plugin
import (
"errors"
"io"
"net/http"
"reflect"
"github.com/mattermost/mattermost/server/public/model"
)
type OnConfigurationChangeIFace interface {
OnConfigurationChange() error
}
type ExecuteCommandIFace interface {
ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
}
type UserHasBeenCreatedIFace interface {
UserHasBeenCreated(c *Context, user *model.User)
}
type UserWillLogInIFace interface {
UserWillLogIn(c *Context, user *model.User) string
}
type UserHasLoggedInIFace interface {
UserHasLoggedIn(c *Context, user *model.User)
}
type MessageWillBePostedIFace interface {
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
}
type MessageWillBeUpdatedIFace interface {
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
}
type MessageHasBeenPostedIFace interface {
MessageHasBeenPosted(c *Context, post *model.Post)
}
type MessageHasBeenUpdatedIFace interface {
MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post)
}
type MessagesWillBeConsumedIFace interface {
MessagesWillBeConsumed(posts []*model.Post) []*model.Post
}
type MessageHasBeenDeletedIFace interface {
MessageHasBeenDeleted(c *Context, post *model.Post)
}
type ChannelHasBeenCreatedIFace interface {
ChannelHasBeenCreated(c *Context, channel *model.Channel)
}
type UserHasJoinedChannelIFace interface {
UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
}
type UserHasLeftChannelIFace interface {
UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
}
type UserHasJoinedTeamIFace interface {
UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
}
type UserHasLeftTeamIFace interface {
UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
}
type FileWillBeUploadedIFace interface {
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
}
type ReactionHasBeenAddedIFace interface {
ReactionHasBeenAdded(c *Context, reaction *model.Reaction)
}
type ReactionHasBeenRemovedIFace interface {
ReactionHasBeenRemoved(c *Context, reaction *model.Reaction)
}
type OnPluginClusterEventIFace interface {
OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent)
}
type OnWebSocketConnectIFace interface {
OnWebSocketConnect(webConnID, userID string)
}
type OnWebSocketDisconnectIFace interface {
OnWebSocketDisconnect(webConnID, userID string)
}
type WebSocketMessageHasBeenPostedIFace interface {
WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest)
}
type RunDataRetentionIFace interface {
RunDataRetention(nowTime, batchSize int64) (int64, error)
}
type OnInstallIFace interface {
OnInstall(c *Context, event model.OnInstallEvent) error
}
type OnSendDailyTelemetryIFace interface {
OnSendDailyTelemetry()
}
type OnCloudLimitsUpdatedIFace interface {
OnCloudLimitsUpdated(limits *model.ProductLimits)
}
type ConfigurationWillBeSavedIFace interface {
ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error)
}
type NotificationWillBePushedIFace interface {
NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string)
}
type UserHasBeenDeactivatedIFace interface {
UserHasBeenDeactivated(c *Context, user *model.User)
}
type ServeMetricsIFace interface {
ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request)
}
type OnSharedChannelsSyncMsgIFace interface {
OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error)
}
type OnSharedChannelsPingIFace interface {
OnSharedChannelsPing(rc *model.RemoteCluster) bool
}
type PreferencesHaveChangedIFace interface {
PreferencesHaveChanged(c *Context, preferences []model.Preference)
}
type OnSharedChannelsAttachmentSyncMsgIFace interface {
OnSharedChannelsAttachmentSyncMsg(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error
}
type OnSharedChannelsProfileImageSyncMsgIFace interface {
OnSharedChannelsProfileImageSyncMsg(user *model.User, rc *model.RemoteCluster) error
}
type HooksAdapter struct {
implemented map[int]struct{}
productHooks any
}
func NewAdapter(productHooks any) (*HooksAdapter, error) {
a := &HooksAdapter{
implemented: make(map[int]struct{}),
productHooks: productHooks,
}
var tt reflect.Type
ft := reflect.TypeOf(productHooks)
// Assessing the type of the productHooks if it individually implements OnConfigurationChange interface.
tt = reflect.TypeOf((*OnConfigurationChangeIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnConfigurationChangeID] = struct{}{}
} else if _, ok := ft.MethodByName("OnConfigurationChange"); ok {
return nil, errors.New("hook has OnConfigurationChange method but does not implement plugin.OnConfigurationChange interface")
}
// Assessing the type of the productHooks if it individually implements ExecuteCommand interface.
tt = reflect.TypeOf((*ExecuteCommandIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ExecuteCommandID] = struct{}{}
} else if _, ok := ft.MethodByName("ExecuteCommand"); ok {
return nil, errors.New("hook has ExecuteCommand method but does not implement plugin.ExecuteCommand interface")
}
// Assessing the type of the productHooks if it individually implements UserHasBeenCreated interface.
tt = reflect.TypeOf((*UserHasBeenCreatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasBeenCreatedID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasBeenCreated"); ok {
return nil, errors.New("hook has UserHasBeenCreated method but does not implement plugin.UserHasBeenCreated interface")
}
// Assessing the type of the productHooks if it individually implements UserWillLogIn interface.
tt = reflect.TypeOf((*UserWillLogInIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserWillLogInID] = struct{}{}
} else if _, ok := ft.MethodByName("UserWillLogIn"); ok {
return nil, errors.New("hook has UserWillLogIn method but does not implement plugin.UserWillLogIn interface")
}
// Assessing the type of the productHooks if it individually implements UserHasLoggedIn interface.
tt = reflect.TypeOf((*UserHasLoggedInIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasLoggedInID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasLoggedIn"); ok {
return nil, errors.New("hook has UserHasLoggedIn method but does not implement plugin.UserHasLoggedIn interface")
}
// Assessing the type of the productHooks if it individually implements MessageWillBePosted interface.
tt = reflect.TypeOf((*MessageWillBePostedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageWillBePostedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageWillBePosted"); ok {
return nil, errors.New("hook has MessageWillBePosted method but does not implement plugin.MessageWillBePosted interface")
}
// Assessing the type of the productHooks if it individually implements MessageWillBeUpdated interface.
tt = reflect.TypeOf((*MessageWillBeUpdatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageWillBeUpdatedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageWillBeUpdated"); ok {
return nil, errors.New("hook has MessageWillBeUpdated method but does not implement plugin.MessageWillBeUpdated interface")
}
// Assessing the type of the productHooks if it individually implements MessageHasBeenPosted interface.
tt = reflect.TypeOf((*MessageHasBeenPostedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageHasBeenPostedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageHasBeenPosted"); ok {
return nil, errors.New("hook has MessageHasBeenPosted method but does not implement plugin.MessageHasBeenPosted interface")
}
// Assessing the type of the productHooks if it individually implements MessageHasBeenUpdated interface.
tt = reflect.TypeOf((*MessageHasBeenUpdatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageHasBeenUpdatedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageHasBeenUpdated"); ok {
return nil, errors.New("hook has MessageHasBeenUpdated method but does not implement plugin.MessageHasBeenUpdated interface")
}
// Assessing the type of the productHooks if it individually implements MessagesWillBeConsumed interface.
tt = reflect.TypeOf((*MessagesWillBeConsumedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessagesWillBeConsumedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessagesWillBeConsumed"); ok {
return nil, errors.New("hook has MessagesWillBeConsumed method but does not implement plugin.MessagesWillBeConsumed interface")
}
// Assessing the type of the productHooks if it individually implements MessageHasBeenDeleted interface.
tt = reflect.TypeOf((*MessageHasBeenDeletedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[MessageHasBeenDeletedID] = struct{}{}
} else if _, ok := ft.MethodByName("MessageHasBeenDeleted"); ok {
return nil, errors.New("hook has MessageHasBeenDeleted method but does not implement plugin.MessageHasBeenDeleted interface")
}
// Assessing the type of the productHooks if it individually implements ChannelHasBeenCreated interface.
tt = reflect.TypeOf((*ChannelHasBeenCreatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ChannelHasBeenCreatedID] = struct{}{}
} else if _, ok := ft.MethodByName("ChannelHasBeenCreated"); ok {
return nil, errors.New("hook has ChannelHasBeenCreated method but does not implement plugin.ChannelHasBeenCreated interface")
}
// Assessing the type of the productHooks if it individually implements UserHasJoinedChannel interface.
tt = reflect.TypeOf((*UserHasJoinedChannelIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasJoinedChannelID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasJoinedChannel"); ok {
return nil, errors.New("hook has UserHasJoinedChannel method but does not implement plugin.UserHasJoinedChannel interface")
}
// Assessing the type of the productHooks if it individually implements UserHasLeftChannel interface.
tt = reflect.TypeOf((*UserHasLeftChannelIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasLeftChannelID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasLeftChannel"); ok {
return nil, errors.New("hook has UserHasLeftChannel method but does not implement plugin.UserHasLeftChannel interface")
}
// Assessing the type of the productHooks if it individually implements UserHasJoinedTeam interface.
tt = reflect.TypeOf((*UserHasJoinedTeamIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasJoinedTeamID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasJoinedTeam"); ok {
return nil, errors.New("hook has UserHasJoinedTeam method but does not implement plugin.UserHasJoinedTeam interface")
}
// Assessing the type of the productHooks if it individually implements UserHasLeftTeam interface.
tt = reflect.TypeOf((*UserHasLeftTeamIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasLeftTeamID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasLeftTeam"); ok {
return nil, errors.New("hook has UserHasLeftTeam method but does not implement plugin.UserHasLeftTeam interface")
}
// Assessing the type of the productHooks if it individually implements FileWillBeUploaded interface.
tt = reflect.TypeOf((*FileWillBeUploadedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[FileWillBeUploadedID] = struct{}{}
} else if _, ok := ft.MethodByName("FileWillBeUploaded"); ok {
return nil, errors.New("hook has FileWillBeUploaded method but does not implement plugin.FileWillBeUploaded interface")
}
// Assessing the type of the productHooks if it individually implements ReactionHasBeenAdded interface.
tt = reflect.TypeOf((*ReactionHasBeenAddedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ReactionHasBeenAddedID] = struct{}{}
} else if _, ok := ft.MethodByName("ReactionHasBeenAdded"); ok {
return nil, errors.New("hook has ReactionHasBeenAdded method but does not implement plugin.ReactionHasBeenAdded interface")
}
// Assessing the type of the productHooks if it individually implements ReactionHasBeenRemoved interface.
tt = reflect.TypeOf((*ReactionHasBeenRemovedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ReactionHasBeenRemovedID] = struct{}{}
} else if _, ok := ft.MethodByName("ReactionHasBeenRemoved"); ok {
return nil, errors.New("hook has ReactionHasBeenRemoved method but does not implement plugin.ReactionHasBeenRemoved interface")
}
// Assessing the type of the productHooks if it individually implements OnPluginClusterEvent interface.
tt = reflect.TypeOf((*OnPluginClusterEventIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnPluginClusterEventID] = struct{}{}
} else if _, ok := ft.MethodByName("OnPluginClusterEvent"); ok {
return nil, errors.New("hook has OnPluginClusterEvent method but does not implement plugin.OnPluginClusterEvent interface")
}
// Assessing the type of the productHooks if it individually implements OnWebSocketConnect interface.
tt = reflect.TypeOf((*OnWebSocketConnectIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnWebSocketConnectID] = struct{}{}
} else if _, ok := ft.MethodByName("OnWebSocketConnect"); ok {
return nil, errors.New("hook has OnWebSocketConnect method but does not implement plugin.OnWebSocketConnect interface")
}
// Assessing the type of the productHooks if it individually implements OnWebSocketDisconnect interface.
tt = reflect.TypeOf((*OnWebSocketDisconnectIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnWebSocketDisconnectID] = struct{}{}
} else if _, ok := ft.MethodByName("OnWebSocketDisconnect"); ok {
return nil, errors.New("hook has OnWebSocketDisconnect method but does not implement plugin.OnWebSocketDisconnect interface")
}
// Assessing the type of the productHooks if it individually implements WebSocketMessageHasBeenPosted interface.
tt = reflect.TypeOf((*WebSocketMessageHasBeenPostedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[WebSocketMessageHasBeenPostedID] = struct{}{}
} else if _, ok := ft.MethodByName("WebSocketMessageHasBeenPosted"); ok {
return nil, errors.New("hook has WebSocketMessageHasBeenPosted method but does not implement plugin.WebSocketMessageHasBeenPosted interface")
}
// Assessing the type of the productHooks if it individually implements RunDataRetention interface.
tt = reflect.TypeOf((*RunDataRetentionIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[RunDataRetentionID] = struct{}{}
} else if _, ok := ft.MethodByName("RunDataRetention"); ok {
return nil, errors.New("hook has RunDataRetention method but does not implement plugin.RunDataRetention interface")
}
// Assessing the type of the productHooks if it individually implements OnInstall interface.
tt = reflect.TypeOf((*OnInstallIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnInstallID] = struct{}{}
} else if _, ok := ft.MethodByName("OnInstall"); ok {
return nil, errors.New("hook has OnInstall method but does not implement plugin.OnInstall interface")
}
// Assessing the type of the productHooks if it individually implements OnSendDailyTelemetry interface.
tt = reflect.TypeOf((*OnSendDailyTelemetryIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnSendDailyTelemetryID] = struct{}{}
} else if _, ok := ft.MethodByName("OnSendDailyTelemetry"); ok {
return nil, errors.New("hook has OnSendDailyTelemetry method but does not implement plugin.OnSendDailyTelemetry interface")
}
// Assessing the type of the productHooks if it individually implements OnCloudLimitsUpdated interface.
tt = reflect.TypeOf((*OnCloudLimitsUpdatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnCloudLimitsUpdatedID] = struct{}{}
} else if _, ok := ft.MethodByName("OnCloudLimitsUpdated"); ok {
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 ConfigurationWillBeSaved interface.
tt = reflect.TypeOf((*ConfigurationWillBeSavedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ConfigurationWillBeSavedID] = struct{}{}
} else if _, ok := ft.MethodByName("ConfigurationWillBeSaved"); ok {
return nil, errors.New("hook has ConfigurationWillBeSaved method but does not implement plugin.ConfigurationWillBeSaved interface")
}
// Assessing the type of the productHooks if it individually implements NotificationWillBePushed interface.
tt = reflect.TypeOf((*NotificationWillBePushedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[NotificationWillBePushedID] = struct{}{}
} else if _, ok := ft.MethodByName("NotificationWillBePushed"); ok {
return nil, errors.New("hook has NotificationWillBePushed method but does not implement plugin.NotificationWillBePushed interface")
}
// Assessing the type of the productHooks if it individually implements UserHasBeenDeactivated interface.
tt = reflect.TypeOf((*UserHasBeenDeactivatedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[UserHasBeenDeactivatedID] = struct{}{}
} else if _, ok := ft.MethodByName("UserHasBeenDeactivated"); ok {
return nil, errors.New("hook has UserHasBeenDeactivated method but does not implement plugin.UserHasBeenDeactivated interface")
}
// Assessing the type of the productHooks if it individually implements ServeMetrics interface.
tt = reflect.TypeOf((*ServeMetricsIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[ServeMetricsID] = struct{}{}
} else if _, ok := ft.MethodByName("ServeMetrics"); ok {
return nil, errors.New("hook has ServeMetrics method but does not implement plugin.ServeMetrics interface")
}
// Assessing the type of the productHooks if it individually implements OnSharedChannelsSyncMsg interface.
tt = reflect.TypeOf((*OnSharedChannelsSyncMsgIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnSharedChannelsSyncMsgID] = struct{}{}
} else if _, ok := ft.MethodByName("OnSharedChannelsSyncMsg"); ok {
return nil, errors.New("hook has OnSharedChannelsSyncMsg method but does not implement plugin.OnSharedChannelsSyncMsg interface")
}
// Assessing the type of the productHooks if it individually implements OnSharedChannelsPing interface.
tt = reflect.TypeOf((*OnSharedChannelsPingIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnSharedChannelsPingID] = struct{}{}
} else if _, ok := ft.MethodByName("OnSharedChannelsPing"); ok {
return nil, errors.New("hook has OnSharedChannelsPing method but does not implement plugin.OnSharedChannelsPing interface")
}
// Assessing the type of the productHooks if it individually implements PreferencesHaveChanged interface.
tt = reflect.TypeOf((*PreferencesHaveChangedIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[PreferencesHaveChangedID] = struct{}{}
} else if _, ok := ft.MethodByName("PreferencesHaveChanged"); ok {
return nil, errors.New("hook has PreferencesHaveChanged method but does not implement plugin.PreferencesHaveChanged interface")
}
// Assessing the type of the productHooks if it individually implements OnSharedChannelsAttachmentSyncMsg interface.
tt = reflect.TypeOf((*OnSharedChannelsAttachmentSyncMsgIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnSharedChannelsAttachmentSyncMsgID] = struct{}{}
} else if _, ok := ft.MethodByName("OnSharedChannelsAttachmentSyncMsg"); ok {
return nil, errors.New("hook has OnSharedChannelsAttachmentSyncMsg method but does not implement plugin.OnSharedChannelsAttachmentSyncMsg interface")
}
// Assessing the type of the productHooks if it individually implements OnSharedChannelsProfileImageSyncMsg interface.
tt = reflect.TypeOf((*OnSharedChannelsProfileImageSyncMsgIFace)(nil)).Elem()
if ft.Implements(tt) {
a.implemented[OnSharedChannelsProfileImageSyncMsgID] = struct{}{}
} else if _, ok := ft.MethodByName("OnSharedChannelsProfileImageSyncMsg"); ok {
return nil, errors.New("hook has OnSharedChannelsProfileImageSyncMsg method but does not implement plugin.OnSharedChannelsProfileImageSyncMsg interface")
}
return a, nil
}
func (a *HooksAdapter) OnConfigurationChange() error {
if _, ok := a.implemented[OnConfigurationChangeID]; !ok {
panic("product hooks must implement OnConfigurationChange")
}
return a.productHooks.(OnConfigurationChangeIFace).OnConfigurationChange()
}
func (a *HooksAdapter) ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
if _, ok := a.implemented[ExecuteCommandID]; !ok {
panic("product hooks must implement ExecuteCommand")
}
return a.productHooks.(ExecuteCommandIFace).ExecuteCommand(c, args)
}
func (a *HooksAdapter) UserHasBeenCreated(c *Context, user *model.User) {
if _, ok := a.implemented[UserHasBeenCreatedID]; !ok {
panic("product hooks must implement UserHasBeenCreated")
}
a.productHooks.(UserHasBeenCreatedIFace).UserHasBeenCreated(c, user)
}
func (a *HooksAdapter) UserWillLogIn(c *Context, user *model.User) string {
if _, ok := a.implemented[UserWillLogInID]; !ok {
panic("product hooks must implement UserWillLogIn")
}
return a.productHooks.(UserWillLogInIFace).UserWillLogIn(c, user)
}
func (a *HooksAdapter) UserHasLoggedIn(c *Context, user *model.User) {
if _, ok := a.implemented[UserHasLoggedInID]; !ok {
panic("product hooks must implement UserHasLoggedIn")
}
a.productHooks.(UserHasLoggedInIFace).UserHasLoggedIn(c, user)
}
func (a *HooksAdapter) MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string) {
if _, ok := a.implemented[MessageWillBePostedID]; !ok {
panic("product hooks must implement MessageWillBePosted")
}
return a.productHooks.(MessageWillBePostedIFace).MessageWillBePosted(c, post)
}
func (a *HooksAdapter) MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string) {
if _, ok := a.implemented[MessageWillBeUpdatedID]; !ok {
panic("product hooks must implement MessageWillBeUpdated")
}
return a.productHooks.(MessageWillBeUpdatedIFace).MessageWillBeUpdated(c, newPost, oldPost)
}
func (a *HooksAdapter) MessageHasBeenPosted(c *Context, post *model.Post) {
if _, ok := a.implemented[MessageHasBeenPostedID]; !ok {
panic("product hooks must implement MessageHasBeenPosted")
}
a.productHooks.(MessageHasBeenPostedIFace).MessageHasBeenPosted(c, post)
}
func (a *HooksAdapter) MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post) {
if _, ok := a.implemented[MessageHasBeenUpdatedID]; !ok {
panic("product hooks must implement MessageHasBeenUpdated")
}
a.productHooks.(MessageHasBeenUpdatedIFace).MessageHasBeenUpdated(c, newPost, oldPost)
}
func (a *HooksAdapter) MessagesWillBeConsumed(posts []*model.Post) []*model.Post {
if _, ok := a.implemented[MessagesWillBeConsumedID]; !ok {
panic("product hooks must implement MessagesWillBeConsumed")
}
return a.productHooks.(MessagesWillBeConsumedIFace).MessagesWillBeConsumed(posts)
}
func (a *HooksAdapter) MessageHasBeenDeleted(c *Context, post *model.Post) {
if _, ok := a.implemented[MessageHasBeenDeletedID]; !ok {
panic("product hooks must implement MessageHasBeenDeleted")
}
a.productHooks.(MessageHasBeenDeletedIFace).MessageHasBeenDeleted(c, post)
}
func (a *HooksAdapter) ChannelHasBeenCreated(c *Context, channel *model.Channel) {
if _, ok := a.implemented[ChannelHasBeenCreatedID]; !ok {
panic("product hooks must implement ChannelHasBeenCreated")
}
a.productHooks.(ChannelHasBeenCreatedIFace).ChannelHasBeenCreated(c, channel)
}
func (a *HooksAdapter) UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
if _, ok := a.implemented[UserHasJoinedChannelID]; !ok {
panic("product hooks must implement UserHasJoinedChannel")
}
a.productHooks.(UserHasJoinedChannelIFace).UserHasJoinedChannel(c, channelMember, actor)
}
func (a *HooksAdapter) UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User) {
if _, ok := a.implemented[UserHasLeftChannelID]; !ok {
panic("product hooks must implement UserHasLeftChannel")
}
a.productHooks.(UserHasLeftChannelIFace).UserHasLeftChannel(c, channelMember, actor)
}
func (a *HooksAdapter) UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
if _, ok := a.implemented[UserHasJoinedTeamID]; !ok {
panic("product hooks must implement UserHasJoinedTeam")
}
a.productHooks.(UserHasJoinedTeamIFace).UserHasJoinedTeam(c, teamMember, actor)
}
func (a *HooksAdapter) UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User) {
if _, ok := a.implemented[UserHasLeftTeamID]; !ok {
panic("product hooks must implement UserHasLeftTeam")
}
a.productHooks.(UserHasLeftTeamIFace).UserHasLeftTeam(c, teamMember, actor)
}
func (a *HooksAdapter) FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) {
if _, ok := a.implemented[FileWillBeUploadedID]; !ok {
panic("product hooks must implement FileWillBeUploaded")
}
return a.productHooks.(FileWillBeUploadedIFace).FileWillBeUploaded(c, info, file, output)
}
func (a *HooksAdapter) ReactionHasBeenAdded(c *Context, reaction *model.Reaction) {
if _, ok := a.implemented[ReactionHasBeenAddedID]; !ok {
panic("product hooks must implement ReactionHasBeenAdded")
}
a.productHooks.(ReactionHasBeenAddedIFace).ReactionHasBeenAdded(c, reaction)
}
func (a *HooksAdapter) ReactionHasBeenRemoved(c *Context, reaction *model.Reaction) {
if _, ok := a.implemented[ReactionHasBeenRemovedID]; !ok {
panic("product hooks must implement ReactionHasBeenRemoved")
}
a.productHooks.(ReactionHasBeenRemovedIFace).ReactionHasBeenRemoved(c, reaction)
}
func (a *HooksAdapter) OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent) {
if _, ok := a.implemented[OnPluginClusterEventID]; !ok {
panic("product hooks must implement OnPluginClusterEvent")
}
a.productHooks.(OnPluginClusterEventIFace).OnPluginClusterEvent(c, ev)
}
func (a *HooksAdapter) OnWebSocketConnect(webConnID, userID string) {
if _, ok := a.implemented[OnWebSocketConnectID]; !ok {
panic("product hooks must implement OnWebSocketConnect")
}
a.productHooks.(OnWebSocketConnectIFace).OnWebSocketConnect(webConnID, userID)
}
func (a *HooksAdapter) OnWebSocketDisconnect(webConnID, userID string) {
if _, ok := a.implemented[OnWebSocketDisconnectID]; !ok {
panic("product hooks must implement OnWebSocketDisconnect")
}
a.productHooks.(OnWebSocketDisconnectIFace).OnWebSocketDisconnect(webConnID, userID)
}
func (a *HooksAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) {
if _, ok := a.implemented[WebSocketMessageHasBeenPostedID]; !ok {
panic("product hooks must implement WebSocketMessageHasBeenPosted")
}
a.productHooks.(WebSocketMessageHasBeenPostedIFace).WebSocketMessageHasBeenPosted(webConnID, userID, req)
}
func (a *HooksAdapter) RunDataRetention(nowTime, batchSize int64) (int64, error) {
if _, ok := a.implemented[RunDataRetentionID]; !ok {
panic("product hooks must implement RunDataRetention")
}
return a.productHooks.(RunDataRetentionIFace).RunDataRetention(nowTime, batchSize)
}
func (a *HooksAdapter) OnInstall(c *Context, event model.OnInstallEvent) error {
if _, ok := a.implemented[OnInstallID]; !ok {
panic("product hooks must implement OnInstall")
}
return a.productHooks.(OnInstallIFace).OnInstall(c, event)
}
func (a *HooksAdapter) OnSendDailyTelemetry() {
if _, ok := a.implemented[OnSendDailyTelemetryID]; !ok {
panic("product hooks must implement OnSendDailyTelemetry")
}
a.productHooks.(OnSendDailyTelemetryIFace).OnSendDailyTelemetry()
}
func (a *HooksAdapter) OnCloudLimitsUpdated(limits *model.ProductLimits) {
if _, ok := a.implemented[OnCloudLimitsUpdatedID]; !ok {
panic("product hooks must implement OnCloudLimitsUpdated")
}
a.productHooks.(OnCloudLimitsUpdatedIFace).OnCloudLimitsUpdated(limits)
}
func (a *HooksAdapter) ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error) {
if _, ok := a.implemented[ConfigurationWillBeSavedID]; !ok {
panic("product hooks must implement ConfigurationWillBeSaved")
}
return a.productHooks.(ConfigurationWillBeSavedIFace).ConfigurationWillBeSaved(newCfg)
}
func (a *HooksAdapter) NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) {
if _, ok := a.implemented[NotificationWillBePushedID]; !ok {
panic("product hooks must implement NotificationWillBePushed")
}
return a.productHooks.(NotificationWillBePushedIFace).NotificationWillBePushed(pushNotification, userID)
}
func (a *HooksAdapter) UserHasBeenDeactivated(c *Context, user *model.User) {
if _, ok := a.implemented[UserHasBeenDeactivatedID]; !ok {
panic("product hooks must implement UserHasBeenDeactivated")
}
a.productHooks.(UserHasBeenDeactivatedIFace).UserHasBeenDeactivated(c, user)
}
func (a *HooksAdapter) ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request) {
if _, ok := a.implemented[ServeMetricsID]; !ok {
panic("product hooks must implement ServeMetrics")
}
a.productHooks.(ServeMetricsIFace).ServeMetrics(c, w, r)
}
func (a *HooksAdapter) OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error) {
if _, ok := a.implemented[OnSharedChannelsSyncMsgID]; !ok {
panic("product hooks must implement OnSharedChannelsSyncMsg")
}
return a.productHooks.(OnSharedChannelsSyncMsgIFace).OnSharedChannelsSyncMsg(msg, rc)
}
func (a *HooksAdapter) OnSharedChannelsPing(rc *model.RemoteCluster) bool {
if _, ok := a.implemented[OnSharedChannelsPingID]; !ok {
panic("product hooks must implement OnSharedChannelsPing")
}
return a.productHooks.(OnSharedChannelsPingIFace).OnSharedChannelsPing(rc)
}
func (a *HooksAdapter) PreferencesHaveChanged(c *Context, preferences []model.Preference) {
if _, ok := a.implemented[PreferencesHaveChangedID]; !ok {
panic("product hooks must implement PreferencesHaveChanged")
}
a.productHooks.(PreferencesHaveChangedIFace).PreferencesHaveChanged(c, preferences)
}
func (a *HooksAdapter) OnSharedChannelsAttachmentSyncMsg(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error {
if _, ok := a.implemented[OnSharedChannelsAttachmentSyncMsgID]; !ok {
panic("product hooks must implement OnSharedChannelsAttachmentSyncMsg")
}
return a.productHooks.(OnSharedChannelsAttachmentSyncMsgIFace).OnSharedChannelsAttachmentSyncMsg(fi, post, rc)
}
func (a *HooksAdapter) OnSharedChannelsProfileImageSyncMsg(user *model.User, rc *model.RemoteCluster) error {
if _, ok := a.implemented[OnSharedChannelsProfileImageSyncMsgID]; !ok {
panic("product hooks must implement OnSharedChannelsProfileImageSyncMsg")
}
return a.productHooks.(OnSharedChannelsProfileImageSyncMsgIFace).OnSharedChannelsProfileImageSyncMsg(user, rc)
}

Просмотреть файл

@@ -143,8 +143,6 @@ type mutex interface {
Unlock()
}
// TODO: this utility function is also used by the product framework. We should move this to mattermost-server and share
// the code to maintain consistent behavior. Ticket: MM-44953
func (b *BotService) ensureBot(m mutex, bot *model.Bot, options ...EnsureBotOption) (string, error) {
err := ensureServerVersion(b.api, "5.10.0")
if err != nil {