Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
49
server/boards/ws/adapter.go
Обычный файл
49
server/boards/ws/adapter.go
Обычный файл
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/mockstore.go -package mocks . Store
|
||||
package ws
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/model"
|
||||
)
|
||||
|
||||
const (
|
||||
websocketActionAuth = "AUTH"
|
||||
websocketActionSubscribeTeam = "SUBSCRIBE_TEAM"
|
||||
websocketActionUnsubscribeTeam = "UNSUBSCRIBE_TEAM"
|
||||
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
||||
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
||||
websocketActionUpdateBoard = "UPDATE_BOARD"
|
||||
websocketActionUpdateMember = "UPDATE_MEMBER"
|
||||
websocketActionDeleteMember = "DELETE_MEMBER"
|
||||
websocketActionUpdateBlock = "UPDATE_BLOCK"
|
||||
websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG"
|
||||
websocketActionUpdateCategory = "UPDATE_CATEGORY"
|
||||
websocketActionUpdateCategoryBoard = "UPDATE_BOARD_CATEGORY"
|
||||
websocketActionUpdateSubscription = "UPDATE_SUBSCRIPTION"
|
||||
websocketActionUpdateCardLimitTimestamp = "UPDATE_CARD_LIMIT_TIMESTAMP"
|
||||
websocketActionReorderCategories = "REORDER_CATEGORIES"
|
||||
websocketActionReorderCategoryBoards = "REORDER_CATEGORY_BOARDS"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
GetBlock(blockID string) (*model.Block, error)
|
||||
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
|
||||
}
|
||||
|
||||
type Adapter interface {
|
||||
BroadcastBlockChange(teamID string, block *model.Block)
|
||||
BroadcastBlockDelete(teamID, blockID, boardID string)
|
||||
BroadcastBoardChange(teamID string, board *model.Board)
|
||||
BroadcastBoardDelete(teamID, boardID string)
|
||||
BroadcastMemberChange(teamID, boardID string, member *model.BoardMember)
|
||||
BroadcastMemberDelete(teamID, boardID, userID string)
|
||||
BroadcastConfigChange(clientConfig model.ClientConfig)
|
||||
BroadcastCategoryChange(category model.Category)
|
||||
BroadcastCategoryBoardChange(teamID, userID string, blockCategory []*model.BoardCategoryWebsocketData)
|
||||
BroadcastCardLimitTimestampChange(cardLimitTimestamp int64)
|
||||
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
||||
BroadcastCategoryReorder(teamID, userID string, categoryOrder []string)
|
||||
BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardsOrder []string)
|
||||
}
|
||||
77
server/boards/ws/common.go
Обычный файл
77
server/boards/ws/common.go
Обычный файл
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/model"
|
||||
)
|
||||
|
||||
// UpdateCategoryMessage is sent on block updates.
|
||||
type UpdateCategoryMessage struct {
|
||||
Action string `json:"action"`
|
||||
TeamID string `json:"teamId"`
|
||||
Category *model.Category `json:"category,omitempty"`
|
||||
BoardCategories []*model.BoardCategoryWebsocketData `json:"blockCategories,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateBlockMsg is sent on block updates.
|
||||
type UpdateBlockMsg struct {
|
||||
Action string `json:"action"`
|
||||
TeamID string `json:"teamId"`
|
||||
Block *model.Block `json:"block"`
|
||||
}
|
||||
|
||||
// UpdateBoardMsg is sent on block updates.
|
||||
type UpdateBoardMsg struct {
|
||||
Action string `json:"action"`
|
||||
TeamID string `json:"teamId"`
|
||||
Board *model.Board `json:"board"`
|
||||
}
|
||||
|
||||
// UpdateMemberMsg is sent on membership updates.
|
||||
type UpdateMemberMsg struct {
|
||||
Action string `json:"action"`
|
||||
TeamID string `json:"teamId"`
|
||||
Member *model.BoardMember `json:"member"`
|
||||
}
|
||||
|
||||
// UpdateSubscription is sent on subscription updates.
|
||||
type UpdateSubscription struct {
|
||||
Action string `json:"action"`
|
||||
Subscription *model.Subscription `json:"subscription"`
|
||||
}
|
||||
|
||||
// UpdateClientConfig is sent on block updates.
|
||||
type UpdateClientConfig struct {
|
||||
Action string `json:"action"`
|
||||
ClientConfig model.ClientConfig `json:"clientconfig"`
|
||||
}
|
||||
|
||||
// UpdateClientConfig is sent on block updates.
|
||||
type UpdateCardLimitTimestamp struct {
|
||||
Action string `json:"action"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WebsocketCommand is an incoming command from the client.
|
||||
type WebsocketCommand struct {
|
||||
Action string `json:"action"`
|
||||
TeamID string `json:"teamId"`
|
||||
Token string `json:"token"`
|
||||
ReadToken string `json:"readToken"`
|
||||
BlockIDs []string `json:"blockIds"`
|
||||
}
|
||||
|
||||
type CategoryReorderMessage struct {
|
||||
Action string `json:"action"`
|
||||
CategoryOrder []string `json:"categoryOrder"`
|
||||
TeamID string `json:"teamId"`
|
||||
}
|
||||
|
||||
type CategoryBoardReorderMessage struct {
|
||||
Action string `json:"action"`
|
||||
CategoryID string `json:"CategoryId"`
|
||||
BoardOrder []string `json:"BoardOrder"`
|
||||
TeamID string `json:"teamId"`
|
||||
}
|
||||
64
server/boards/ws/helpers_test.go
Обычный файл
64
server/boards/ws/helpers_test.go
Обычный файл
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
authMocks "github.com/mattermost/mattermost-server/v6/server/boards/auth/mocks"
|
||||
wsMocks "github.com/mattermost/mattermost-server/v6/server/boards/ws/mocks"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
api *wsMocks.MockAPI
|
||||
auth *authMocks.MockAuthInterface
|
||||
store *wsMocks.MockStore
|
||||
ctrl *gomock.Controller
|
||||
pa *PluginAdapter
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) *TestHelper {
|
||||
ctrl := gomock.NewController(t)
|
||||
mockAPI := wsMocks.NewMockAPI(ctrl)
|
||||
mockAuth := authMocks.NewMockAuthInterface(ctrl)
|
||||
mockStore := wsMocks.NewMockStore(ctrl)
|
||||
|
||||
mockAPI.EXPECT().LogDebug(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
mockAPI.EXPECT().LogInfo(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
mockAPI.EXPECT().LogError(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
mockAPI.EXPECT().LogWarn(gomock.Any(), gomock.Any()).AnyTimes()
|
||||
|
||||
return &TestHelper{
|
||||
api: mockAPI,
|
||||
auth: mockAuth,
|
||||
store: mockStore,
|
||||
ctrl: ctrl,
|
||||
pa: NewPluginAdapter(mockAPI, mockAuth, mockStore, mlog.CreateConsoleTestLogger(true, mlog.LvlDebug)),
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) ReceiveWebSocketMessage(webConnID, userID, action string, data map[string]interface{}) {
|
||||
req := &mm_model.WebSocketRequest{Action: websocketMessagePrefix + action, Data: data}
|
||||
|
||||
th.pa.WebSocketMessageHasBeenPosted(webConnID, userID, req)
|
||||
}
|
||||
|
||||
func (th *TestHelper) SubscribeWebConnToTeam(webConnID, userID, teamID string) {
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID, teamID).
|
||||
Return(true)
|
||||
|
||||
msgData := map[string]interface{}{"teamId": teamID}
|
||||
th.ReceiveWebSocketMessage(webConnID, userID, websocketActionSubscribeTeam, msgData)
|
||||
}
|
||||
|
||||
func (th *TestHelper) UnsubscribeWebConnFromTeam(webConnID, userID, teamID string) {
|
||||
msgData := map[string]interface{}{"teamId": teamID}
|
||||
th.ReceiveWebSocketMessage(webConnID, userID, websocketActionUnsubscribeTeam, msgData)
|
||||
}
|
||||
2659
server/boards/ws/mocks/mockpluginapi.go
Обычный файл
2659
server/boards/ws/mocks/mockpluginapi.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
68
server/boards/ws/mocks/mockstore.go
Обычный файл
68
server/boards/ws/mocks/mockstore.go
Обычный файл
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/mattermost/mattermost-server/v6/server/boards/ws (interfaces: Store)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
model "github.com/mattermost/mattermost-server/v6/server/boards/model"
|
||||
)
|
||||
|
||||
// MockStore is a mock of Store interface.
|
||||
type MockStore struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStoreMockRecorder
|
||||
}
|
||||
|
||||
// MockStoreMockRecorder is the mock recorder for MockStore.
|
||||
type MockStoreMockRecorder struct {
|
||||
mock *MockStore
|
||||
}
|
||||
|
||||
// NewMockStore creates a new mock instance.
|
||||
func NewMockStore(ctrl *gomock.Controller) *MockStore {
|
||||
mock := &MockStore{ctrl: ctrl}
|
||||
mock.recorder = &MockStoreMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockStore) EXPECT() *MockStoreMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetBlock mocks base method.
|
||||
func (m *MockStore) GetBlock(arg0 string) (*model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBlock", arg0)
|
||||
ret0, _ := ret[0].(*model.Block)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetBlock indicates an expected call of GetBlock.
|
||||
func (mr *MockStoreMockRecorder) GetBlock(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockStore)(nil).GetBlock), arg0)
|
||||
}
|
||||
|
||||
// GetMembersForBoard mocks base method.
|
||||
func (m *MockStore) GetMembersForBoard(arg0 string) ([]*model.BoardMember, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetMembersForBoard", arg0)
|
||||
ret0, _ := ret[0].([]*model.BoardMember)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetMembersForBoard indicates an expected call of GetMembersForBoard.
|
||||
func (mr *MockStoreMockRecorder) GetMembersForBoard(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembersForBoard", reflect.TypeOf((*MockStore)(nil).GetMembersForBoard), arg0)
|
||||
}
|
||||
678
server/boards/ws/plugin_adapter.go
Обычный файл
678
server/boards/ws/plugin_adapter.go
Обычный файл
@@ -0,0 +1,678 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
const websocketMessagePrefix = "custom_boards_"
|
||||
|
||||
var errMissingTeamInCommand = fmt.Errorf("command doesn't contain teamId")
|
||||
|
||||
type PluginAdapterInterface interface {
|
||||
Adapter
|
||||
OnWebSocketConnect(webConnID, userID string)
|
||||
OnWebSocketDisconnect(webConnID, userID string)
|
||||
WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest)
|
||||
BroadcastConfigChange(clientConfig model.ClientConfig)
|
||||
BroadcastBlockChange(teamID string, block *model.Block)
|
||||
BroadcastBlockDelete(teamID, blockID, parentID string)
|
||||
BroadcastSubscriptionChange(teamID string, subscription *model.Subscription)
|
||||
BroadcastCardLimitTimestampChange(cardLimitTimestamp int64)
|
||||
HandleClusterEvent(ev mm_model.PluginClusterEvent)
|
||||
}
|
||||
|
||||
type PluginAdapter struct {
|
||||
api servicesAPI
|
||||
auth auth.AuthInterface
|
||||
staleThreshold time.Duration
|
||||
store Store
|
||||
logger mlog.LoggerIFace
|
||||
|
||||
listenersMU sync.RWMutex
|
||||
listeners map[string]*PluginAdapterClient
|
||||
listenersByUserID map[string][]*PluginAdapterClient
|
||||
|
||||
subscriptionsMU sync.RWMutex
|
||||
listenersByTeam map[string][]*PluginAdapterClient
|
||||
listenersByBlock map[string][]*PluginAdapterClient
|
||||
}
|
||||
|
||||
// servicesAPI is the interface required by the PluginAdapter to interact with
|
||||
// the mattermost-server.
|
||||
type servicesAPI interface {
|
||||
PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast)
|
||||
PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error
|
||||
}
|
||||
|
||||
func NewPluginAdapter(api servicesAPI, auth auth.AuthInterface, store Store, logger mlog.LoggerIFace) *PluginAdapter {
|
||||
return &PluginAdapter{
|
||||
api: api,
|
||||
auth: auth,
|
||||
store: store,
|
||||
staleThreshold: 5 * time.Minute,
|
||||
logger: logger,
|
||||
listeners: make(map[string]*PluginAdapterClient),
|
||||
listenersByUserID: make(map[string][]*PluginAdapterClient),
|
||||
listenersByTeam: make(map[string][]*PluginAdapterClient),
|
||||
listenersByBlock: make(map[string][]*PluginAdapterClient),
|
||||
listenersMU: sync.RWMutex{},
|
||||
subscriptionsMU: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) GetListenerByWebConnID(webConnID string) (pac *PluginAdapterClient, ok bool) {
|
||||
pa.listenersMU.RLock()
|
||||
defer pa.listenersMU.RUnlock()
|
||||
|
||||
pac, ok = pa.listeners[webConnID]
|
||||
return
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) GetListenersByUserID(userID string) []*PluginAdapterClient {
|
||||
pa.listenersMU.RLock()
|
||||
defer pa.listenersMU.RUnlock()
|
||||
|
||||
return pa.listenersByUserID[userID]
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) GetListenersByTeam(teamID string) []*PluginAdapterClient {
|
||||
pa.subscriptionsMU.RLock()
|
||||
defer pa.subscriptionsMU.RUnlock()
|
||||
|
||||
return pa.listenersByTeam[teamID]
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) GetListenersByBlock(blockID string) []*PluginAdapterClient {
|
||||
pa.subscriptionsMU.RLock()
|
||||
defer pa.subscriptionsMU.RUnlock()
|
||||
|
||||
return pa.listenersByBlock[blockID]
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) addListener(pac *PluginAdapterClient) {
|
||||
pa.listenersMU.Lock()
|
||||
defer pa.listenersMU.Unlock()
|
||||
|
||||
pa.listeners[pac.webConnID] = pac
|
||||
pa.listenersByUserID[pac.userID] = append(pa.listenersByUserID[pac.userID], pac)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) removeListener(pac *PluginAdapterClient) {
|
||||
pa.listenersMU.Lock()
|
||||
defer pa.listenersMU.Unlock()
|
||||
|
||||
// team subscriptions
|
||||
for _, team := range pac.teams {
|
||||
pa.removeListenerFromTeam(pac, team)
|
||||
}
|
||||
|
||||
// block subscriptions
|
||||
for _, block := range pac.blocks {
|
||||
pa.removeListenerFromBlock(pac, block)
|
||||
}
|
||||
|
||||
// user ID list
|
||||
newUserListeners := []*PluginAdapterClient{}
|
||||
for _, listener := range pa.listenersByUserID[pac.userID] {
|
||||
if listener.webConnID != pac.webConnID {
|
||||
newUserListeners = append(newUserListeners, listener)
|
||||
}
|
||||
}
|
||||
pa.listenersByUserID[pac.userID] = newUserListeners
|
||||
|
||||
delete(pa.listeners, pac.webConnID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) removeExpiredForUserID(userID string) {
|
||||
for _, pac := range pa.GetListenersByUserID(userID) {
|
||||
if !pac.isActive() && pac.hasExpired(pa.staleThreshold) {
|
||||
pa.removeListener(pac)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) removeListenerFromTeam(pac *PluginAdapterClient, teamID string) {
|
||||
newTeamListeners := []*PluginAdapterClient{}
|
||||
for _, listener := range pa.GetListenersByTeam(teamID) {
|
||||
if listener.webConnID != pac.webConnID {
|
||||
newTeamListeners = append(newTeamListeners, listener)
|
||||
}
|
||||
}
|
||||
pa.subscriptionsMU.Lock()
|
||||
pa.listenersByTeam[teamID] = newTeamListeners
|
||||
pa.subscriptionsMU.Unlock()
|
||||
|
||||
pac.unsubscribeFromTeam(teamID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) removeListenerFromBlock(pac *PluginAdapterClient, blockID string) {
|
||||
newBlockListeners := []*PluginAdapterClient{}
|
||||
for _, listener := range pa.GetListenersByBlock(blockID) {
|
||||
if listener.webConnID != pac.webConnID {
|
||||
newBlockListeners = append(newBlockListeners, listener)
|
||||
}
|
||||
}
|
||||
pa.subscriptionsMU.Lock()
|
||||
pa.listenersByBlock[blockID] = newBlockListeners
|
||||
pa.subscriptionsMU.Unlock()
|
||||
|
||||
pac.unsubscribeFromBlock(blockID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) subscribeListenerToTeam(pac *PluginAdapterClient, teamID string) {
|
||||
if pac.isSubscribedToTeam(teamID) {
|
||||
return
|
||||
}
|
||||
|
||||
pa.subscriptionsMU.Lock()
|
||||
pa.listenersByTeam[teamID] = append(pa.listenersByTeam[teamID], pac)
|
||||
pa.subscriptionsMU.Unlock()
|
||||
|
||||
pac.subscribeToTeam(teamID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) unsubscribeListenerFromTeam(pac *PluginAdapterClient, teamID string) {
|
||||
if !pac.isSubscribedToTeam(teamID) {
|
||||
return
|
||||
}
|
||||
|
||||
pa.removeListenerFromTeam(pac, teamID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) getUserIDsForTeam(teamID string) []string {
|
||||
userMap := map[string]bool{}
|
||||
for _, pac := range pa.GetListenersByTeam(teamID) {
|
||||
if pac.isActive() {
|
||||
userMap[pac.userID] = true
|
||||
}
|
||||
}
|
||||
|
||||
userIDs := []string{}
|
||||
for userID := range userMap {
|
||||
if pa.auth.DoesUserHaveTeamAccess(userID, teamID) {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
}
|
||||
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) getUserIDsForTeamAndBoard(teamID, boardID string, ensureUserIDs ...string) []string {
|
||||
userMap := map[string]bool{}
|
||||
for _, pac := range pa.GetListenersByTeam(teamID) {
|
||||
if pac.isActive() {
|
||||
userMap[pac.userID] = true
|
||||
}
|
||||
}
|
||||
|
||||
members, err := pa.store.GetMembersForBoard(boardID)
|
||||
if err != nil {
|
||||
pa.logger.Error("error getting members for board",
|
||||
mlog.String("method", "getUserIDsForTeamAndBoard"),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// the list of users would be the intersection between the ones
|
||||
// that are connected to the team and the board members that need
|
||||
// to see the updates
|
||||
userIDs := []string{}
|
||||
for _, member := range members {
|
||||
for userID := range userMap {
|
||||
if userID == member.UserID && pa.auth.DoesUserHaveTeamAccess(userID, teamID) {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we don't have to make sure that some IDs are included, we
|
||||
// can return at this point
|
||||
if len(ensureUserIDs) == 0 {
|
||||
return userIDs
|
||||
}
|
||||
|
||||
completeUserMap := map[string]bool{}
|
||||
for _, id := range userIDs {
|
||||
completeUserMap[id] = true
|
||||
}
|
||||
for _, id := range ensureUserIDs {
|
||||
completeUserMap[id] = true
|
||||
}
|
||||
|
||||
completeUserIDs := []string{}
|
||||
for id := range completeUserMap {
|
||||
completeUserIDs = append(completeUserIDs, id)
|
||||
}
|
||||
|
||||
return completeUserIDs
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (pa *PluginAdapter) unsubscribeListenerFromBlocks(pac *PluginAdapterClient, blockIDs []string) {
|
||||
for _, blockID := range blockIDs {
|
||||
if pac.isSubscribedToBlock(blockID) {
|
||||
pa.removeListenerFromBlock(pac, blockID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) OnWebSocketConnect(webConnID, userID string) {
|
||||
if existingPAC, ok := pa.GetListenerByWebConnID(webConnID); ok {
|
||||
pa.logger.Debug("inactive connection found for webconn, reusing",
|
||||
mlog.String("webConnID", webConnID),
|
||||
mlog.String("userID", userID),
|
||||
)
|
||||
atomic.StoreInt64(&existingPAC.inactiveAt, 0)
|
||||
return
|
||||
}
|
||||
|
||||
newPAC := &PluginAdapterClient{
|
||||
inactiveAt: 0,
|
||||
webConnID: webConnID,
|
||||
userID: userID,
|
||||
teams: []string{},
|
||||
blocks: []string{},
|
||||
}
|
||||
|
||||
pa.addListener(newPAC)
|
||||
pa.removeExpiredForUserID(userID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
pac, ok := pa.GetListenerByWebConnID(webConnID)
|
||||
if !ok {
|
||||
pa.logger.Debug("received a disconnect for an unregistered webconn",
|
||||
mlog.String("webConnID", webConnID),
|
||||
mlog.String("userID", userID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
atomic.StoreInt64(&pac.inactiveAt, mm_model.GetMillis())
|
||||
}
|
||||
|
||||
func commandFromRequest(req *mm_model.WebSocketRequest) (*WebsocketCommand, error) {
|
||||
c := &WebsocketCommand{Action: strings.TrimPrefix(req.Action, websocketMessagePrefix)}
|
||||
|
||||
if teamID, ok := req.Data["teamId"]; ok {
|
||||
c.TeamID = teamID.(string)
|
||||
} else {
|
||||
return nil, errMissingTeamInCommand
|
||||
}
|
||||
|
||||
if readToken, ok := req.Data["readToken"]; ok {
|
||||
c.ReadToken = readToken.(string)
|
||||
}
|
||||
|
||||
if blockIDs, ok := req.Data["blockIds"]; ok {
|
||||
c.BlockIDs = blockIDs.([]string)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mm_model.WebSocketRequest) {
|
||||
pac, ok := pa.GetListenerByWebConnID(webConnID)
|
||||
if !ok {
|
||||
pa.logger.Debug("received a message for an unregistered webconn",
|
||||
mlog.String("webConnID", webConnID),
|
||||
mlog.String("userID", userID),
|
||||
mlog.String("action", req.Action),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// only process messages using the plugin actions
|
||||
if !strings.HasPrefix(req.Action, websocketMessagePrefix) {
|
||||
return
|
||||
}
|
||||
|
||||
command, err := commandFromRequest(req)
|
||||
if err != nil {
|
||||
pa.logger.Error("error getting command from request",
|
||||
mlog.String("action", req.Action),
|
||||
mlog.String("webConnID", webConnID),
|
||||
mlog.String("userID", userID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switch command.Action {
|
||||
// The block-related commands are not implemented in the adapter
|
||||
// as there is no such thing as unauthenticated websocket
|
||||
// connections in plugin mode. Only a debug line is logged
|
||||
case websocketActionSubscribeBlocks, websocketActionUnsubscribeBlocks:
|
||||
pa.logger.Debug(`Command not implemented in plugin mode`,
|
||||
mlog.String("command", command.Action),
|
||||
mlog.String("webConnID", webConnID),
|
||||
mlog.String("userID", userID),
|
||||
mlog.String("teamID", command.TeamID),
|
||||
)
|
||||
|
||||
case websocketActionSubscribeTeam:
|
||||
pa.logger.Debug(`Command not implemented in plugin mode`,
|
||||
mlog.String("command", command.Action),
|
||||
mlog.String("webConnID", webConnID),
|
||||
mlog.String("userID", userID),
|
||||
mlog.String("teamID", command.TeamID),
|
||||
)
|
||||
|
||||
if !pa.auth.DoesUserHaveTeamAccess(userID, command.TeamID) {
|
||||
return
|
||||
}
|
||||
|
||||
pa.subscribeListenerToTeam(pac, command.TeamID)
|
||||
case websocketActionUnsubscribeTeam:
|
||||
pa.logger.Debug(`Command: UNSUBSCRIBE_WORKSPACE`,
|
||||
mlog.String("webConnID", webConnID),
|
||||
mlog.String("userID", userID),
|
||||
mlog.String("teamID", command.TeamID),
|
||||
)
|
||||
|
||||
pa.unsubscribeListenerFromTeam(pac, command.TeamID)
|
||||
}
|
||||
}
|
||||
|
||||
// sendMessageToAll will send a websocket message to all clients on all nodes.
|
||||
func (pa *PluginAdapter) sendMessageToAll(event string, payload map[string]interface{}) {
|
||||
// Empty &mm_model.WebsocketBroadcast will send to all users
|
||||
pa.api.PublishWebSocketEvent(event, payload, &mm_model.WebsocketBroadcast{})
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastConfigChange(pluginConfig model.ClientConfig) {
|
||||
pa.sendMessageToAll(websocketActionUpdateConfig, utils.StructToMap(pluginConfig))
|
||||
}
|
||||
|
||||
// sendUserMessageSkipCluster sends the message to specific users.
|
||||
func (pa *PluginAdapter) sendUserMessageSkipCluster(event string, payload map[string]interface{}, userIDs ...string) {
|
||||
for _, userID := range userIDs {
|
||||
pa.api.PublishWebSocketEvent(event, payload, &mm_model.WebsocketBroadcast{UserId: userID})
|
||||
}
|
||||
}
|
||||
|
||||
// sendTeamMessageSkipCluster sends a message to all the users
|
||||
// with a websocket client subscribed to a given team.
|
||||
func (pa *PluginAdapter) sendTeamMessageSkipCluster(event, teamID string, payload map[string]interface{}) {
|
||||
userIDs := pa.getUserIDsForTeam(teamID)
|
||||
pa.sendUserMessageSkipCluster(event, payload, userIDs...)
|
||||
}
|
||||
|
||||
// sendTeamMessage sends and propagates a message that is aimed
|
||||
// for all the users that are subscribed to a given team.
|
||||
func (pa *PluginAdapter) sendTeamMessage(event, teamID string, payload map[string]interface{}, ensureUserIDs ...string) {
|
||||
go func() {
|
||||
clusterMessage := &ClusterMessage{
|
||||
TeamID: teamID,
|
||||
Payload: payload,
|
||||
EnsureUsers: ensureUserIDs,
|
||||
}
|
||||
|
||||
pa.sendMessageToCluster(clusterMessage)
|
||||
}()
|
||||
|
||||
pa.sendTeamMessageSkipCluster(event, teamID, payload)
|
||||
}
|
||||
|
||||
// sendBoardMessageSkipCluster sends a message to all the users
|
||||
// subscribed to a given team that belong to one of its boards.
|
||||
func (pa *PluginAdapter) sendBoardMessageSkipCluster(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) {
|
||||
userIDs := pa.getUserIDsForTeamAndBoard(teamID, boardID, ensureUserIDs...)
|
||||
pa.sendUserMessageSkipCluster(websocketActionUpdateBoard, payload, userIDs...)
|
||||
}
|
||||
|
||||
// sendBoardMessage sends and propagates a message that is aimed for
|
||||
// all the users that are subscribed to the board's team and are
|
||||
// members of it too.
|
||||
func (pa *PluginAdapter) sendBoardMessage(teamID, boardID string, payload map[string]interface{}, ensureUserIDs ...string) {
|
||||
go func() {
|
||||
clusterMessage := &ClusterMessage{
|
||||
TeamID: teamID,
|
||||
BoardID: boardID,
|
||||
Payload: payload,
|
||||
EnsureUsers: ensureUserIDs,
|
||||
}
|
||||
|
||||
pa.sendMessageToCluster(clusterMessage)
|
||||
}()
|
||||
|
||||
pa.sendBoardMessageSkipCluster(teamID, boardID, payload, ensureUserIDs...)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBlockChange(teamID string, block *model.Block) {
|
||||
pa.logger.Trace("BroadcastingBlockChange",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", block.BoardID),
|
||||
mlog.String("blockID", block.ID),
|
||||
)
|
||||
|
||||
message := UpdateBlockMsg{
|
||||
Action: websocketActionUpdateBlock,
|
||||
TeamID: teamID,
|
||||
Block: block,
|
||||
}
|
||||
|
||||
pa.sendBoardMessage(teamID, block.BoardID, utils.StructToMap(message))
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastCategoryChange(category model.Category) {
|
||||
pa.logger.Debug("BroadcastCategoryChange",
|
||||
mlog.String("userID", category.UserID),
|
||||
mlog.String("teamID", category.TeamID),
|
||||
mlog.String("categoryID", category.ID),
|
||||
)
|
||||
|
||||
message := UpdateCategoryMessage{
|
||||
Action: websocketActionUpdateCategory,
|
||||
TeamID: category.TeamID,
|
||||
Category: &category,
|
||||
}
|
||||
|
||||
payload := utils.StructToMap(message)
|
||||
|
||||
go func() {
|
||||
clusterMessage := &ClusterMessage{
|
||||
Payload: payload,
|
||||
UserID: category.UserID,
|
||||
}
|
||||
|
||||
pa.sendMessageToCluster(clusterMessage)
|
||||
}()
|
||||
|
||||
pa.sendUserMessageSkipCluster(websocketActionUpdateCategory, payload, category.UserID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastCategoryReorder(teamID, userID string, categoryOrder []string) {
|
||||
pa.logger.Debug("BroadcastCategoryReorder",
|
||||
mlog.String("userID", userID),
|
||||
mlog.String("teamID", teamID),
|
||||
)
|
||||
|
||||
message := CategoryReorderMessage{
|
||||
Action: websocketActionReorderCategories,
|
||||
CategoryOrder: categoryOrder,
|
||||
TeamID: teamID,
|
||||
}
|
||||
payload := utils.StructToMap(message)
|
||||
go func() {
|
||||
clusterMessage := &ClusterMessage{
|
||||
Payload: payload,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
pa.sendMessageToCluster(clusterMessage)
|
||||
}()
|
||||
|
||||
pa.sendUserMessageSkipCluster(message.Action, payload, userID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardsOrder []string) {
|
||||
pa.logger.Debug("BroadcastCategoryBoardsReorder",
|
||||
mlog.String("userID", userID),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("categoryID", categoryID),
|
||||
)
|
||||
|
||||
message := CategoryBoardReorderMessage{
|
||||
Action: websocketActionReorderCategoryBoards,
|
||||
CategoryID: categoryID,
|
||||
BoardOrder: boardsOrder,
|
||||
TeamID: teamID,
|
||||
}
|
||||
payload := utils.StructToMap(message)
|
||||
go func() {
|
||||
clusterMessage := &ClusterMessage{
|
||||
Payload: payload,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
pa.sendMessageToCluster(clusterMessage)
|
||||
}()
|
||||
|
||||
pa.sendUserMessageSkipCluster(message.Action, payload, userID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastCategoryBoardChange(teamID, userID string, boardCategories []*model.BoardCategoryWebsocketData) {
|
||||
pa.logger.Debug(
|
||||
"BroadcastCategoryBoardChange",
|
||||
mlog.String("userID", userID),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.Int("numEntries", len(boardCategories)),
|
||||
)
|
||||
|
||||
message := UpdateCategoryMessage{
|
||||
Action: websocketActionUpdateCategoryBoard,
|
||||
TeamID: teamID,
|
||||
BoardCategories: boardCategories,
|
||||
}
|
||||
|
||||
payload := utils.StructToMap(message)
|
||||
|
||||
go func() {
|
||||
clusterMessage := &ClusterMessage{
|
||||
Payload: payload,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
pa.sendMessageToCluster(clusterMessage)
|
||||
}()
|
||||
|
||||
pa.sendUserMessageSkipCluster(websocketActionUpdateCategoryBoard, utils.StructToMap(message), userID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBlockDelete(teamID, blockID, boardID string) {
|
||||
now := utils.GetMillis()
|
||||
block := &model.Block{}
|
||||
block.ID = blockID
|
||||
block.BoardID = boardID
|
||||
block.UpdateAt = now
|
||||
block.DeleteAt = now
|
||||
|
||||
pa.BroadcastBlockChange(teamID, block)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBoardChange(teamID string, board *model.Board) {
|
||||
pa.logger.Debug("BroadcastingBoardChange",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", board.ID),
|
||||
)
|
||||
|
||||
message := UpdateBoardMsg{
|
||||
Action: websocketActionUpdateBoard,
|
||||
TeamID: teamID,
|
||||
Board: board,
|
||||
}
|
||||
|
||||
pa.sendBoardMessage(teamID, board.ID, utils.StructToMap(message))
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBoardDelete(teamID, boardID string) {
|
||||
now := utils.GetMillis()
|
||||
board := &model.Board{}
|
||||
board.ID = boardID
|
||||
board.TeamID = teamID
|
||||
board.UpdateAt = now
|
||||
board.DeleteAt = now
|
||||
|
||||
pa.BroadcastBoardChange(teamID, board)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) {
|
||||
pa.logger.Debug("BroadcastingMemberChange",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
mlog.String("userID", member.UserID),
|
||||
)
|
||||
|
||||
message := UpdateMemberMsg{
|
||||
Action: websocketActionUpdateMember,
|
||||
TeamID: teamID,
|
||||
Member: member,
|
||||
}
|
||||
|
||||
pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message), member.UserID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastMemberDelete(teamID, boardID, userID string) {
|
||||
pa.logger.Debug("BroadcastingMemberDelete",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
mlog.String("userID", userID),
|
||||
)
|
||||
|
||||
message := UpdateMemberMsg{
|
||||
Action: websocketActionDeleteMember,
|
||||
TeamID: teamID,
|
||||
Member: &model.BoardMember{UserID: userID, BoardID: boardID},
|
||||
}
|
||||
|
||||
// when fetching the members of the board that should receive the
|
||||
// member deletion message, the deleted member will not be one of
|
||||
// them, so we need to ensure they receive the message
|
||||
pa.sendBoardMessage(teamID, boardID, utils.StructToMap(message), userID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastSubscriptionChange(teamID string, subscription *model.Subscription) {
|
||||
pa.logger.Debug("BroadcastingSubscriptionChange",
|
||||
mlog.String("TeamID", teamID),
|
||||
mlog.String("blockID", subscription.BlockID),
|
||||
mlog.String("subscriberID", subscription.SubscriberID),
|
||||
)
|
||||
|
||||
message := UpdateSubscription{
|
||||
Action: websocketActionUpdateSubscription,
|
||||
Subscription: subscription,
|
||||
}
|
||||
|
||||
pa.sendTeamMessage(websocketActionUpdateSubscription, teamID, utils.StructToMap(message))
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
|
||||
pa.logger.Debug("BroadcastCardLimitTimestampChange",
|
||||
mlog.Int64("cardLimitTimestamp", cardLimitTimestamp),
|
||||
)
|
||||
|
||||
message := UpdateCardLimitTimestamp{
|
||||
Action: websocketActionUpdateCardLimitTimestamp,
|
||||
Timestamp: cardLimitTimestamp,
|
||||
}
|
||||
|
||||
pa.sendMessageToAll(websocketActionUpdateCardLimitTimestamp, utils.StructToMap(message))
|
||||
}
|
||||
89
server/boards/ws/plugin_adapter_client.go
Обычный файл
89
server/boards/ws/plugin_adapter_client.go
Обычный файл
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
type PluginAdapterClient struct {
|
||||
inactiveAt int64
|
||||
webConnID string
|
||||
userID string
|
||||
teams []string
|
||||
blocks []string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (pac *PluginAdapterClient) isActive() bool {
|
||||
return atomic.LoadInt64(&pac.inactiveAt) == 0
|
||||
}
|
||||
|
||||
func (pac *PluginAdapterClient) hasExpired(threshold time.Duration) bool {
|
||||
return !mm_model.GetTimeForMillis(atomic.LoadInt64(&pac.inactiveAt)).Add(threshold).After(time.Now())
|
||||
}
|
||||
|
||||
func (pac *PluginAdapterClient) subscribeToTeam(teamID string) {
|
||||
pac.mu.Lock()
|
||||
defer pac.mu.Unlock()
|
||||
|
||||
pac.teams = append(pac.teams, teamID)
|
||||
}
|
||||
|
||||
func (pac *PluginAdapterClient) unsubscribeFromTeam(teamID string) {
|
||||
pac.mu.Lock()
|
||||
defer pac.mu.Unlock()
|
||||
|
||||
newClientTeams := []string{}
|
||||
for _, id := range pac.teams {
|
||||
if id != teamID {
|
||||
newClientTeams = append(newClientTeams, id)
|
||||
}
|
||||
}
|
||||
pac.teams = newClientTeams
|
||||
}
|
||||
|
||||
func (pac *PluginAdapterClient) unsubscribeFromBlock(blockID string) {
|
||||
pac.mu.Lock()
|
||||
defer pac.mu.Unlock()
|
||||
|
||||
newClientBlocks := []string{}
|
||||
for _, id := range pac.blocks {
|
||||
if id != blockID {
|
||||
newClientBlocks = append(newClientBlocks, id)
|
||||
}
|
||||
}
|
||||
pac.blocks = newClientBlocks
|
||||
}
|
||||
|
||||
func (pac *PluginAdapterClient) isSubscribedToTeam(teamID string) bool {
|
||||
pac.mu.RLock()
|
||||
defer pac.mu.RUnlock()
|
||||
|
||||
for _, id := range pac.teams {
|
||||
if id == teamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (pac *PluginAdapterClient) isSubscribedToBlock(blockID string) bool {
|
||||
pac.mu.RLock()
|
||||
defer pac.mu.RUnlock()
|
||||
|
||||
for _, id := range pac.blocks {
|
||||
if id == blockID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
83
server/boards/ws/plugin_adapter_cluster.go
Обычный файл
83
server/boards/ws/plugin_adapter_cluster.go
Обычный файл
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type ClusterMessage struct {
|
||||
TeamID string
|
||||
BoardID string
|
||||
UserID string
|
||||
Payload map[string]interface{}
|
||||
EnsureUsers []string
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) sendMessageToCluster(clusterMessage *ClusterMessage) {
|
||||
const id = "websocket_message"
|
||||
b, err := json.Marshal(clusterMessage)
|
||||
if err != nil {
|
||||
pa.logger.Error("couldn't get JSON bytes from cluster message",
|
||||
mlog.String("id", id),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
event := mm_model.PluginClusterEvent{Id: id, Data: b}
|
||||
opts := mm_model.PluginClusterEventSendOptions{
|
||||
SendType: mm_model.PluginClusterEventSendTypeReliable,
|
||||
}
|
||||
|
||||
if err := pa.api.PublishPluginClusterEvent(event, opts); err != nil {
|
||||
pa.logger.Error("error publishing cluster event",
|
||||
mlog.String("id", id),
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) HandleClusterEvent(ev mm_model.PluginClusterEvent) {
|
||||
pa.logger.Debug("received cluster event", mlog.String("id", ev.Id))
|
||||
|
||||
var clusterMessage ClusterMessage
|
||||
if err := json.Unmarshal(ev.Data, &clusterMessage); err != nil {
|
||||
pa.logger.Error("cannot unmarshal cluster message data",
|
||||
mlog.String("id", ev.Id),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if clusterMessage.BoardID != "" {
|
||||
pa.sendBoardMessageSkipCluster(clusterMessage.TeamID, clusterMessage.BoardID, clusterMessage.Payload, clusterMessage.EnsureUsers...)
|
||||
return
|
||||
}
|
||||
|
||||
var action string
|
||||
if actionRaw, ok := clusterMessage.Payload["action"]; ok {
|
||||
if s, ok := actionRaw.(string); ok {
|
||||
action = s
|
||||
}
|
||||
}
|
||||
if action == "" {
|
||||
// no action was specified in the event; assume block change and warn.
|
||||
pa.logger.Warn("cannot determine action from cluster message data",
|
||||
mlog.String("id", ev.Id),
|
||||
mlog.Map("payload", clusterMessage.Payload),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if clusterMessage.UserID != "" {
|
||||
pa.sendUserMessageSkipCluster(action, clusterMessage.Payload, clusterMessage.UserID)
|
||||
return
|
||||
}
|
||||
|
||||
pa.sendTeamMessageSkipCluster(action, clusterMessage.TeamID, clusterMessage.Payload)
|
||||
}
|
||||
556
server/boards/ws/plugin_adapter_test.go
Обычный файл
556
server/boards/ws/plugin_adapter_test.go
Обычный файл
@@ -0,0 +1,556 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/model"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPluginAdapterTeamSubscription(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
|
||||
webConnID := mm_model.NewId()
|
||||
userID := mm_model.NewId()
|
||||
teamID := mm_model.NewId()
|
||||
|
||||
var pac *PluginAdapterClient
|
||||
t.Run("Should correctly add a connection", func(t *testing.T) {
|
||||
require.Empty(t, th.pa.listeners)
|
||||
require.Empty(t, th.pa.listenersByTeam)
|
||||
th.pa.OnWebSocketConnect(webConnID, userID)
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
|
||||
var ok bool
|
||||
pac, ok = th.pa.listeners[webConnID]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, pac)
|
||||
require.Equal(t, userID, pac.userID)
|
||||
require.Empty(t, th.pa.listenersByTeam)
|
||||
})
|
||||
|
||||
t.Run("Should correctly subscribe to a team", func(t *testing.T) {
|
||||
require.False(t, pac.isSubscribedToTeam(teamID))
|
||||
|
||||
th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID)
|
||||
|
||||
require.Len(t, th.pa.listenersByTeam[teamID], 1)
|
||||
require.Contains(t, th.pa.listenersByTeam[teamID], pac)
|
||||
require.Len(t, pac.teams, 1)
|
||||
require.Contains(t, pac.teams, teamID)
|
||||
|
||||
require.True(t, pac.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Subscribing again to a subscribed team would have no effect", func(t *testing.T) {
|
||||
require.True(t, pac.isSubscribedToTeam(teamID))
|
||||
|
||||
th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID)
|
||||
|
||||
require.Len(t, th.pa.listenersByTeam[teamID], 1)
|
||||
require.Contains(t, th.pa.listenersByTeam[teamID], pac)
|
||||
require.Len(t, pac.teams, 1)
|
||||
require.Contains(t, pac.teams, teamID)
|
||||
|
||||
require.True(t, pac.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Should correctly unsubscribe to a team", func(t *testing.T) {
|
||||
require.True(t, pac.isSubscribedToTeam(teamID))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID)
|
||||
|
||||
require.Empty(t, th.pa.listenersByTeam[teamID])
|
||||
require.Empty(t, pac.teams)
|
||||
|
||||
require.False(t, pac.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Unsubscribing again to an unsubscribed team would have no effect", func(t *testing.T) {
|
||||
require.False(t, pac.isSubscribedToTeam(teamID))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(pac.webConnID, pac.userID, teamID)
|
||||
|
||||
require.Empty(t, th.pa.listenersByTeam[teamID])
|
||||
require.Empty(t, pac.teams)
|
||||
|
||||
require.False(t, pac.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Should correctly be marked as inactive if disconnected", func(t *testing.T) {
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.True(t, th.pa.listeners[webConnID].isActive())
|
||||
|
||||
th.pa.OnWebSocketDisconnect(webConnID, userID)
|
||||
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.False(t, th.pa.listeners[webConnID].isActive())
|
||||
})
|
||||
|
||||
t.Run("Should be marked back as active if reconnect", func(t *testing.T) {
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.False(t, th.pa.listeners[webConnID].isActive())
|
||||
|
||||
th.pa.OnWebSocketConnect(webConnID, userID)
|
||||
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.True(t, th.pa.listeners[webConnID].isActive())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPluginAdapterClientReconnect(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
|
||||
webConnID := mm_model.NewId()
|
||||
userID := mm_model.NewId()
|
||||
teamID := mm_model.NewId()
|
||||
|
||||
var pac *PluginAdapterClient
|
||||
t.Run("A user should be able to reconnect within the accepted threshold and keep their subscriptions", func(t *testing.T) {
|
||||
// create the connection
|
||||
require.Len(t, th.pa.listeners, 0)
|
||||
require.Len(t, th.pa.listenersByUserID[userID], 0)
|
||||
th.pa.OnWebSocketConnect(webConnID, userID)
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.Len(t, th.pa.listenersByUserID[userID], 1)
|
||||
var ok bool
|
||||
pac, ok = th.pa.listeners[webConnID]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, pac)
|
||||
|
||||
th.SubscribeWebConnToTeam(pac.webConnID, pac.userID, teamID)
|
||||
require.True(t, pac.isSubscribedToTeam(teamID))
|
||||
|
||||
// disconnect
|
||||
th.pa.OnWebSocketDisconnect(webConnID, userID)
|
||||
require.False(t, pac.isActive())
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.Len(t, th.pa.listenersByUserID[userID], 1)
|
||||
|
||||
// reconnect right away. The connection should still be subscribed
|
||||
th.pa.OnWebSocketConnect(webConnID, userID)
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.Len(t, th.pa.listenersByUserID[userID], 1)
|
||||
require.True(t, pac.isActive())
|
||||
require.True(t, pac.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Should remove old inactive connection when user connects with a different ID", func(t *testing.T) {
|
||||
// we set the stale threshold to zero so inactive connections always get deleted
|
||||
oldStaleThreshold := th.pa.staleThreshold
|
||||
th.pa.staleThreshold = 0
|
||||
defer func() { th.pa.staleThreshold = oldStaleThreshold }()
|
||||
th.pa.OnWebSocketDisconnect(webConnID, userID)
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.Len(t, th.pa.listenersByUserID[userID], 1)
|
||||
require.Equal(t, webConnID, th.pa.listenersByUserID[userID][0].webConnID)
|
||||
|
||||
newWebConnID := mm_model.NewId()
|
||||
th.pa.OnWebSocketConnect(newWebConnID, userID)
|
||||
|
||||
require.Len(t, th.pa.listeners, 1)
|
||||
require.Len(t, th.pa.listenersByUserID[userID], 1)
|
||||
require.Contains(t, th.pa.listeners, newWebConnID)
|
||||
require.NotContains(t, th.pa.listeners, webConnID)
|
||||
require.Equal(t, newWebConnID, th.pa.listenersByUserID[userID][0].webConnID)
|
||||
|
||||
// if the same ID connects again, it should have no subscriptions
|
||||
th.pa.OnWebSocketConnect(webConnID, userID)
|
||||
require.Len(t, th.pa.listeners, 2)
|
||||
require.Len(t, th.pa.listenersByUserID[userID], 2)
|
||||
reconnectedPAC, ok := th.pa.listeners[webConnID]
|
||||
require.True(t, ok)
|
||||
require.False(t, reconnectedPAC.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Should not remove active connections when user connects with a different ID", func(t *testing.T) {
|
||||
// we set the stale threshold to zero so inactive connections always get deleted
|
||||
oldStaleThreshold := th.pa.staleThreshold
|
||||
th.pa.staleThreshold = 0
|
||||
defer func() { th.pa.staleThreshold = oldStaleThreshold }()
|
||||
|
||||
// currently we have two listeners for userID, both active
|
||||
require.Len(t, th.pa.listeners, 2)
|
||||
|
||||
// a new user connects
|
||||
th.pa.OnWebSocketConnect(mm_model.NewId(), userID)
|
||||
|
||||
// and we should have three connections, all of them active
|
||||
require.Len(t, th.pa.listeners, 3)
|
||||
|
||||
for _, listener := range th.pa.listeners {
|
||||
require.True(t, listener.isActive())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUserIDsForTeam(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
|
||||
// we have two teams
|
||||
teamID1 := mm_model.NewId()
|
||||
teamID2 := mm_model.NewId()
|
||||
|
||||
// user 1 has two connections
|
||||
userID1 := mm_model.NewId()
|
||||
webConnID1 := mm_model.NewId()
|
||||
webConnID2 := mm_model.NewId()
|
||||
|
||||
// user 2 has one connection
|
||||
userID2 := mm_model.NewId()
|
||||
webConnID3 := mm_model.NewId()
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(3)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.pa.OnWebSocketConnect(webConnID1, userID1)
|
||||
th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1)
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.pa.OnWebSocketConnect(webConnID2, userID1)
|
||||
th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2)
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.pa.OnWebSocketConnect(webConnID3, userID2)
|
||||
th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2)
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
t.Run("should find that only user1 is connected to team 1", func(t *testing.T) {
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID1).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeam(teamID1)
|
||||
require.ElementsMatch(t, []string{userID1}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should find that both users are connected to team 2", func(t *testing.T) {
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID2, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeam(teamID2)
|
||||
require.ElementsMatch(t, []string{userID1, userID2}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should ignore user1 if webConn 2 inactive when getting team 2 user ids", func(t *testing.T) {
|
||||
th.pa.OnWebSocketDisconnect(webConnID2, userID1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID2, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeam(teamID2)
|
||||
require.ElementsMatch(t, []string{userID2}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should still find user 1 in team 1 after the webConn 2 disconnection", func(t *testing.T) {
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID1).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeam(teamID1)
|
||||
require.ElementsMatch(t, []string{userID1}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should find again both users if the webConn 2 comes back", func(t *testing.T) {
|
||||
th.pa.OnWebSocketConnect(webConnID2, userID1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID2, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeam(teamID2)
|
||||
require.ElementsMatch(t, []string{userID1, userID2}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should only find user 1 if user 2 has an active connection but is not a team member anymore", func(t *testing.T) {
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
// userID2 does not have team access
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID2, teamID2).
|
||||
Return(false).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeam(teamID2)
|
||||
require.ElementsMatch(t, []string{userID1}, userIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUserIDsForTeamAndBoard(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
|
||||
// we have two teams
|
||||
teamID1 := mm_model.NewId()
|
||||
boardID1 := mm_model.NewId()
|
||||
teamID2 := mm_model.NewId()
|
||||
boardID2 := mm_model.NewId()
|
||||
|
||||
// user 1 has two connections
|
||||
userID1 := mm_model.NewId()
|
||||
webConnID1 := mm_model.NewId()
|
||||
webConnID2 := mm_model.NewId()
|
||||
|
||||
// user 2 has one connection
|
||||
userID2 := mm_model.NewId()
|
||||
webConnID3 := mm_model.NewId()
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(3)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.pa.OnWebSocketConnect(webConnID1, userID1)
|
||||
th.SubscribeWebConnToTeam(webConnID1, userID1, teamID1)
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.pa.OnWebSocketConnect(webConnID2, userID1)
|
||||
th.SubscribeWebConnToTeam(webConnID2, userID1, teamID2)
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.pa.OnWebSocketConnect(webConnID3, userID2)
|
||||
th.SubscribeWebConnToTeam(webConnID3, userID2, teamID2)
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
t.Run("should find that only user1 is connected to team 1 and board 1", func(t *testing.T) {
|
||||
mockedMembers := []*model.BoardMember{{UserID: userID1}}
|
||||
th.store.EXPECT().
|
||||
GetMembersForBoard(boardID1).
|
||||
Return(mockedMembers, nil).
|
||||
Times(1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID1).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeamAndBoard(teamID1, boardID1)
|
||||
require.ElementsMatch(t, []string{userID1}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should find that both users are connected to team 2 and board 2", func(t *testing.T) {
|
||||
mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
|
||||
th.store.EXPECT().
|
||||
GetMembersForBoard(boardID2).
|
||||
Return(mockedMembers, nil).
|
||||
Times(1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID2, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
|
||||
require.ElementsMatch(t, []string{userID1, userID2}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should find that only one user is connected to team 2 and board 2 if there is only one membership with both connected", func(t *testing.T) {
|
||||
mockedMembers := []*model.BoardMember{{UserID: userID1}}
|
||||
th.store.EXPECT().
|
||||
GetMembersForBoard(boardID2).
|
||||
Return(mockedMembers, nil).
|
||||
Times(1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
|
||||
require.ElementsMatch(t, []string{userID1}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should find only one if the other is inactive", func(t *testing.T) {
|
||||
th.pa.OnWebSocketDisconnect(webConnID3, userID2)
|
||||
defer th.pa.OnWebSocketConnect(webConnID3, userID2)
|
||||
|
||||
mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
|
||||
th.store.EXPECT().
|
||||
GetMembersForBoard(boardID2).
|
||||
Return(mockedMembers, nil).
|
||||
Times(1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
|
||||
require.ElementsMatch(t, []string{userID1}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should include a user that is not present if it's ensured", func(t *testing.T) {
|
||||
userID3 := mm_model.NewId()
|
||||
mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
|
||||
th.store.EXPECT().
|
||||
GetMembersForBoard(boardID2).
|
||||
Return(mockedMembers, nil).
|
||||
Times(1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID2, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2, userID3)
|
||||
require.ElementsMatch(t, []string{userID1, userID2, userID3}, userIDs)
|
||||
})
|
||||
|
||||
t.Run("should not include a user that, although present, has no team access anymore", func(t *testing.T) {
|
||||
mockedMembers := []*model.BoardMember{{UserID: userID1}, {UserID: userID2}}
|
||||
th.store.EXPECT().
|
||||
GetMembersForBoard(boardID2).
|
||||
Return(mockedMembers, nil).
|
||||
Times(1)
|
||||
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID1, teamID2).
|
||||
Return(true).
|
||||
Times(1)
|
||||
|
||||
// userID2 has no team access
|
||||
th.auth.EXPECT().
|
||||
DoesUserHaveTeamAccess(userID2, teamID2).
|
||||
Return(false).
|
||||
Times(1)
|
||||
|
||||
userIDs := th.pa.getUserIDsForTeamAndBoard(teamID2, boardID2)
|
||||
require.ElementsMatch(t, []string{userID1}, userIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParallelSubscriptionsOnMultipleConnections(t *testing.T) {
|
||||
th := SetupTestHelper(t)
|
||||
|
||||
teamID1 := mm_model.NewId()
|
||||
teamID2 := mm_model.NewId()
|
||||
teamID3 := mm_model.NewId()
|
||||
teamID4 := mm_model.NewId()
|
||||
|
||||
userID := mm_model.NewId()
|
||||
webConnID1 := mm_model.NewId()
|
||||
webConnID2 := mm_model.NewId()
|
||||
|
||||
th.pa.OnWebSocketConnect(webConnID1, userID)
|
||||
pac1, ok := th.pa.GetListenerByWebConnID(webConnID1)
|
||||
require.True(t, ok)
|
||||
|
||||
th.pa.OnWebSocketConnect(webConnID2, userID)
|
||||
pac2, ok := th.pa.GetListenerByWebConnID(webConnID2)
|
||||
require.True(t, ok)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(4)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.SubscribeWebConnToTeam(webConnID1, userID, teamID1)
|
||||
require.True(t, pac1.isSubscribedToTeam(teamID1))
|
||||
|
||||
th.SubscribeWebConnToTeam(webConnID2, userID, teamID1)
|
||||
require.True(t, pac2.isSubscribedToTeam(teamID1))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID1)
|
||||
require.False(t, pac1.isSubscribedToTeam(teamID1))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID1)
|
||||
require.False(t, pac2.isSubscribedToTeam(teamID1))
|
||||
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.SubscribeWebConnToTeam(webConnID1, userID, teamID2)
|
||||
require.True(t, pac1.isSubscribedToTeam(teamID2))
|
||||
|
||||
th.SubscribeWebConnToTeam(webConnID2, userID, teamID2)
|
||||
require.True(t, pac2.isSubscribedToTeam(teamID2))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID2)
|
||||
require.False(t, pac1.isSubscribedToTeam(teamID2))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID2)
|
||||
require.False(t, pac2.isSubscribedToTeam(teamID2))
|
||||
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.SubscribeWebConnToTeam(webConnID1, userID, teamID3)
|
||||
require.True(t, pac1.isSubscribedToTeam(teamID3))
|
||||
|
||||
th.SubscribeWebConnToTeam(webConnID2, userID, teamID3)
|
||||
require.True(t, pac2.isSubscribedToTeam(teamID3))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID3)
|
||||
require.False(t, pac1.isSubscribedToTeam(teamID3))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID3)
|
||||
require.False(t, pac2.isSubscribedToTeam(teamID3))
|
||||
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
th.SubscribeWebConnToTeam(webConnID1, userID, teamID4)
|
||||
require.True(t, pac1.isSubscribedToTeam(teamID4))
|
||||
|
||||
th.SubscribeWebConnToTeam(webConnID2, userID, teamID4)
|
||||
require.True(t, pac2.isSubscribedToTeam(teamID4))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID1, userID, teamID4)
|
||||
require.False(t, pac1.isSubscribedToTeam(teamID4))
|
||||
|
||||
th.UnsubscribeWebConnFromTeam(webConnID2, userID, teamID4)
|
||||
require.False(t, pac2.isSubscribedToTeam(teamID4))
|
||||
|
||||
wg.Done()
|
||||
}(wg)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
810
server/boards/ws/server.go
Обычный файл
810
server/boards/ws/server.go
Обычный файл
@@ -0,0 +1,810 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
func (wss *websocketSession) WriteJSON(v interface{}) error {
|
||||
wss.mu.Lock()
|
||||
defer wss.mu.Unlock()
|
||||
err := wss.conn.WriteJSON(v)
|
||||
return err
|
||||
}
|
||||
|
||||
func (wss *websocketSession) isSubscribedToTeam(teamID string) bool {
|
||||
for _, id := range wss.teams {
|
||||
if id == teamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (wss *websocketSession) isSubscribedToBlock(blockID string) bool {
|
||||
for _, id := range wss.blocks {
|
||||
if id == blockID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Server is a WebSocket server.
|
||||
type Server struct {
|
||||
upgrader websocket.Upgrader
|
||||
listeners map[*websocketSession]bool
|
||||
listenersByTeam map[string][]*websocketSession
|
||||
listenersByBlock map[string][]*websocketSession
|
||||
mu sync.RWMutex
|
||||
auth *auth.Auth
|
||||
singleUserToken string
|
||||
isMattermostAuth bool
|
||||
logger mlog.LoggerIFace
|
||||
store Store
|
||||
}
|
||||
|
||||
type websocketSession struct {
|
||||
conn *websocket.Conn
|
||||
userID string
|
||||
mu sync.Mutex
|
||||
teams []string
|
||||
blocks []string
|
||||
}
|
||||
|
||||
func (wss *websocketSession) isAuthenticated() bool {
|
||||
return wss.userID != ""
|
||||
}
|
||||
|
||||
// NewServer creates a new Server.
|
||||
func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool, logger mlog.LoggerIFace, store Store) *Server {
|
||||
return &Server{
|
||||
listeners: make(map[*websocketSession]bool),
|
||||
listenersByTeam: make(map[string][]*websocketSession),
|
||||
listenersByBlock: make(map[string][]*websocketSession),
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
auth: auth,
|
||||
singleUserToken: singleUserToken,
|
||||
isMattermostAuth: isMattermostAuth,
|
||||
logger: logger,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers routes.
|
||||
func (ws *Server) RegisterRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/ws", ws.handleWebSocket)
|
||||
}
|
||||
|
||||
func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade initial GET request to a websocket
|
||||
client, err := ws.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
ws.logger.Error("ERROR upgrading to websocket", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
// create an empty session with websocket client
|
||||
wsSession := &websocketSession{
|
||||
conn: client,
|
||||
userID: "",
|
||||
mu: sync.Mutex{},
|
||||
teams: []string{},
|
||||
blocks: []string{},
|
||||
}
|
||||
|
||||
if ws.isMattermostAuth {
|
||||
wsSession.userID = r.Header.Get("Mattermost-User-Id")
|
||||
}
|
||||
|
||||
ws.addListener(wsSession)
|
||||
|
||||
// Make sure we close the connection when the function returns
|
||||
defer func() {
|
||||
ws.logger.Debug("DISCONNECT WebSocket", mlog.Stringer("client", wsSession.conn.RemoteAddr()))
|
||||
|
||||
// Remove session from listeners
|
||||
ws.removeListener(wsSession)
|
||||
wsSession.conn.Close()
|
||||
}()
|
||||
|
||||
// Simple message handling loop
|
||||
for {
|
||||
_, p, err := wsSession.conn.ReadMessage()
|
||||
if err != nil {
|
||||
ws.logger.Error("ERROR WebSocket",
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
mlog.Err(err),
|
||||
)
|
||||
ws.removeListener(wsSession)
|
||||
break
|
||||
}
|
||||
|
||||
var command WebsocketCommand
|
||||
|
||||
err = json.Unmarshal(p, &command)
|
||||
if err != nil {
|
||||
// handle this error
|
||||
ws.logger.Error(`ERROR webSocket parsing command`, mlog.String("json", string(p)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if command.Action == websocketActionAuth {
|
||||
ws.logger.Debug(`Command: AUTH`, mlog.Stringer("client", wsSession.conn.RemoteAddr()))
|
||||
ws.authenticateListener(wsSession, command.Token)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// if the client wants to subscribe to a set of blocks and it
|
||||
// is sending a read token, we don't need to check for
|
||||
// authentication
|
||||
if command.Action == websocketActionSubscribeBlocks {
|
||||
ws.logger.Debug(`Command: SUBSCRIBE_BLOCKS`,
|
||||
mlog.String("teamID", command.TeamID),
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
if !ws.isCommandReadTokenValid(command) {
|
||||
ws.logger.Error(`Rejected invalid read token`,
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
mlog.String("action", command.Action),
|
||||
mlog.String("readToken", command.ReadToken),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ws.subscribeListenerToBlocks(wsSession, command.BlockIDs)
|
||||
continue
|
||||
}
|
||||
|
||||
if command.Action == websocketActionUnsubscribeBlocks {
|
||||
ws.logger.Debug(`Command: UNSUBSCRIBE_BLOCKS`,
|
||||
mlog.String("teamID", command.TeamID),
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
if !ws.isCommandReadTokenValid(command) {
|
||||
ws.logger.Error(`Rejected invalid read token`,
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
mlog.String("action", command.Action),
|
||||
mlog.String("readToken", command.ReadToken),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ws.unsubscribeListenerFromBlocks(wsSession, command.BlockIDs)
|
||||
continue
|
||||
}
|
||||
|
||||
// if the command is not authenticated at this point, it will
|
||||
// not be processed
|
||||
if !wsSession.isAuthenticated() {
|
||||
ws.logger.Error(`Rejected unauthenticated message`,
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
mlog.String("action", command.Action),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
switch command.Action {
|
||||
case websocketActionSubscribeTeam:
|
||||
ws.logger.Debug(`Command: SUBSCRIBE_TEAM`,
|
||||
mlog.String("teamID", command.TeamID),
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
// if single user mode, check that the userID is valid and
|
||||
// assume that the user has permission if so
|
||||
if ws.singleUserToken != "" {
|
||||
if wsSession.userID != model.SingleUser {
|
||||
continue
|
||||
}
|
||||
|
||||
// if not in single user mode validate that the session
|
||||
// has permissions to the team
|
||||
} else {
|
||||
ws.logger.Debug("Not single user mode")
|
||||
if !ws.auth.DoesUserHaveTeamAccess(wsSession.userID, command.TeamID) {
|
||||
ws.logger.Error("WS user doesn't have team access", mlog.String("teamID", command.TeamID), mlog.String("userID", wsSession.userID))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ws.subscribeListenerToTeam(wsSession, command.TeamID)
|
||||
case websocketActionUnsubscribeTeam:
|
||||
ws.logger.Debug(`Command: UNSUBSCRIBE_TEAM`,
|
||||
mlog.String("teamID", command.TeamID),
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
ws.unsubscribeListenerFromTeam(wsSession, command.TeamID)
|
||||
default:
|
||||
ws.logger.Error(`ERROR webSocket command, invalid action`, mlog.String("action", command.Action))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isCommandReadTokenValid ensures that a command contains a read
|
||||
// token and a set of block ids that said token is valid for.
|
||||
func (ws *Server) isCommandReadTokenValid(command WebsocketCommand) bool {
|
||||
if command.TeamID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
boardID := ""
|
||||
// all the blocks must be part of the same board
|
||||
for _, blockID := range command.BlockIDs {
|
||||
block, err := ws.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if boardID == "" {
|
||||
boardID = block.BoardID
|
||||
continue
|
||||
}
|
||||
|
||||
if boardID != block.BoardID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// the read token must be valid for the board
|
||||
isValid, err := ws.auth.IsValidReadToken(boardID, command.ReadToken)
|
||||
if err != nil {
|
||||
ws.logger.Error(`ERROR when checking token validity`,
|
||||
mlog.String("teamID", command.TeamID),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
// addListener adds a listener to the websocket server. The listener
|
||||
// should not receive any update from the server until it subscribes
|
||||
// itself to some entity changes. Adding a listener to the server
|
||||
// doesn't mean that it's authenticated in any way.
|
||||
func (ws *Server) addListener(listener *websocketSession) {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
ws.listeners[listener] = true
|
||||
}
|
||||
|
||||
// removeListener removes a listener and all its subscriptions, if
|
||||
// any, from the websockets server.
|
||||
func (ws *Server) removeListener(listener *websocketSession) {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
// remove the listener from its subscriptions, if any
|
||||
|
||||
// team subscriptions
|
||||
for _, team := range listener.teams {
|
||||
ws.removeListenerFromTeam(listener, team)
|
||||
}
|
||||
|
||||
// block subscriptions
|
||||
for _, block := range listener.blocks {
|
||||
ws.removeListenerFromBlock(listener, block)
|
||||
}
|
||||
|
||||
delete(ws.listeners, listener)
|
||||
}
|
||||
|
||||
// subscribeListenerToTeam safely modifies the listener and the
|
||||
// server to subscribe the listener to a given team updates.
|
||||
func (ws *Server) subscribeListenerToTeam(listener *websocketSession, teamID string) {
|
||||
if listener.isSubscribedToTeam(teamID) {
|
||||
return
|
||||
}
|
||||
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
ws.listenersByTeam[teamID] = append(ws.listenersByTeam[teamID], listener)
|
||||
listener.teams = append(listener.teams, teamID)
|
||||
}
|
||||
|
||||
// unsubscribeListenerFromTeam safely modifies the listener and
|
||||
// the server data structures to remove the link between the listener
|
||||
// and a given team ID.
|
||||
func (ws *Server) unsubscribeListenerFromTeam(listener *websocketSession, teamID string) {
|
||||
if !listener.isSubscribedToTeam(teamID) {
|
||||
return
|
||||
}
|
||||
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
ws.removeListenerFromTeam(listener, teamID)
|
||||
}
|
||||
|
||||
// subscribeListenerToBlocks safely modifies the listener and the
|
||||
// server to subscribe the listener to a given set of block updates.
|
||||
func (ws *Server) subscribeListenerToBlocks(listener *websocketSession, blockIDs []string) {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
for _, blockID := range blockIDs {
|
||||
if listener.isSubscribedToBlock(blockID) {
|
||||
continue
|
||||
}
|
||||
|
||||
ws.listenersByBlock[blockID] = append(ws.listenersByBlock[blockID], listener)
|
||||
listener.blocks = append(listener.blocks, blockID)
|
||||
}
|
||||
}
|
||||
|
||||
// unsubscribeListenerFromBlocks safely modifies the listener and the
|
||||
// server data structures to remove the link between the listener and
|
||||
// a given set of block IDs.
|
||||
func (ws *Server) unsubscribeListenerFromBlocks(listener *websocketSession, blockIDs []string) {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
for _, blockID := range blockIDs {
|
||||
if listener.isSubscribedToBlock(blockID) {
|
||||
ws.removeListenerFromBlock(listener, blockID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeListenerFromTeam removes the listener from both its own
|
||||
// block subscribed list and the server listeners by team map.
|
||||
func (ws *Server) removeListenerFromTeam(listener *websocketSession, teamID string) {
|
||||
// we remove the listener from the team index
|
||||
newTeamListeners := []*websocketSession{}
|
||||
for _, l := range ws.listenersByTeam[teamID] {
|
||||
if l != listener {
|
||||
newTeamListeners = append(newTeamListeners, l)
|
||||
}
|
||||
}
|
||||
ws.listenersByTeam[teamID] = newTeamListeners
|
||||
|
||||
// we remove the team from the listener subscription list
|
||||
newListenerTeams := []string{}
|
||||
for _, id := range listener.teams {
|
||||
if id != teamID {
|
||||
newListenerTeams = append(newListenerTeams, id)
|
||||
}
|
||||
}
|
||||
listener.teams = newListenerTeams
|
||||
}
|
||||
|
||||
// removeListenerFromBlock removes the listener from both its own
|
||||
// block subscribed list and the server listeners by block map.
|
||||
func (ws *Server) removeListenerFromBlock(listener *websocketSession, blockID string) {
|
||||
// we remove the listener from the block index
|
||||
newBlockListeners := []*websocketSession{}
|
||||
for _, l := range ws.listenersByBlock[blockID] {
|
||||
if l != listener {
|
||||
newBlockListeners = append(newBlockListeners, l)
|
||||
}
|
||||
}
|
||||
ws.listenersByBlock[blockID] = newBlockListeners
|
||||
|
||||
// we remove the block from the listener subscription list
|
||||
newListenerBlocks := []string{}
|
||||
for _, id := range listener.blocks {
|
||||
if id != blockID {
|
||||
newListenerBlocks = append(newListenerBlocks, id)
|
||||
}
|
||||
}
|
||||
listener.blocks = newListenerBlocks
|
||||
}
|
||||
|
||||
func (ws *Server) getUserIDForToken(token string) string {
|
||||
if ws.singleUserToken != "" {
|
||||
if token == ws.singleUserToken {
|
||||
return model.SingleUser
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
session, err := ws.auth.GetSession(token)
|
||||
if session == nil || err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return session.UserID
|
||||
}
|
||||
|
||||
func (ws *Server) authenticateListener(wsSession *websocketSession, token string) {
|
||||
ws.logger.Debug("authenticateListener",
|
||||
mlog.String("token", token),
|
||||
mlog.String("wsSession.userID", wsSession.userID),
|
||||
)
|
||||
if wsSession.isAuthenticated() {
|
||||
// Do not allow multiple auth calls (for security)
|
||||
ws.logger.Debug(
|
||||
"authenticateListener: Ignoring already authenticated session",
|
||||
mlog.String("userID", wsSession.userID),
|
||||
mlog.Stringer("client", wsSession.conn.RemoteAddr()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate session
|
||||
userID := ws.getUserIDForToken(token)
|
||||
if userID == "" {
|
||||
wsSession.conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticated
|
||||
wsSession.userID = userID
|
||||
ws.logger.Debug("authenticateListener: Authenticated", mlog.String("userID", userID), mlog.Stringer("client", wsSession.conn.RemoteAddr()))
|
||||
}
|
||||
|
||||
// getListenersForBlock returns the listeners subscribed to a
|
||||
// block changes.
|
||||
func (ws *Server) getListenersForBlock(blockID string) []*websocketSession {
|
||||
return ws.listenersByBlock[blockID]
|
||||
}
|
||||
|
||||
// getListenersForTeam returns the listeners subscribed to a
|
||||
// team changes.
|
||||
func (ws *Server) getListenersForTeam(teamID string) []*websocketSession {
|
||||
return ws.listenersByTeam[teamID]
|
||||
}
|
||||
|
||||
// getListenersForTeamAndBoard returns the listeners subscribed to a
|
||||
// team changes and members of a given board.
|
||||
func (ws *Server) getListenersForTeamAndBoard(teamID, boardID string, ensureUsers ...string) []*websocketSession {
|
||||
members, err := ws.store.GetMembersForBoard(boardID)
|
||||
if err != nil {
|
||||
ws.logger.Error("error getting members for board",
|
||||
mlog.String("method", "getListenersForTeamAndBoard"),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
memberMap := map[string]bool{}
|
||||
for _, member := range members {
|
||||
memberMap[member.UserID] = true
|
||||
}
|
||||
for _, id := range ensureUsers {
|
||||
memberMap[id] = true
|
||||
}
|
||||
|
||||
memberIDs := []string{}
|
||||
for id := range memberMap {
|
||||
memberIDs = append(memberIDs, id)
|
||||
}
|
||||
|
||||
listeners := []*websocketSession{}
|
||||
for _, memberID := range memberIDs {
|
||||
for _, listener := range ws.listenersByTeam[teamID] {
|
||||
if listener.userID == memberID {
|
||||
listeners = append(listeners, listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
return listeners
|
||||
}
|
||||
|
||||
// BroadcastBlockDelete broadcasts delete messages to clients.
|
||||
func (ws *Server) BroadcastBlockDelete(teamID, blockID, boardID string) {
|
||||
now := utils.GetMillis()
|
||||
block := &model.Block{}
|
||||
block.ID = blockID
|
||||
block.BoardID = boardID
|
||||
block.UpdateAt = now
|
||||
block.DeleteAt = now
|
||||
|
||||
ws.BroadcastBlockChange(teamID, block)
|
||||
}
|
||||
|
||||
// BroadcastBlockChange broadcasts update messages to clients.
|
||||
func (ws *Server) BroadcastBlockChange(teamID string, block *model.Block) {
|
||||
blockIDsToNotify := []string{block.ID, block.ParentID}
|
||||
|
||||
message := UpdateBlockMsg{
|
||||
Action: websocketActionUpdateBlock,
|
||||
TeamID: teamID,
|
||||
Block: block,
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForTeamAndBoard(teamID, block.BoardID)
|
||||
ws.logger.Trace("listener(s) for teamID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", block.BoardID),
|
||||
)
|
||||
|
||||
for _, blockID := range blockIDsToNotify {
|
||||
listeners = append(listeners, ws.getListenersForBlock(blockID)...)
|
||||
ws.logger.Trace("listener(s) for blockID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("blockID", blockID),
|
||||
)
|
||||
}
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast block change",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("blockID", block.ID),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
ws.logger.Error("broadcast error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastCategoryChange(category model.Category) {
|
||||
message := UpdateCategoryMessage{
|
||||
Action: websocketActionUpdateCategory,
|
||||
TeamID: category.TeamID,
|
||||
Category: &category,
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForTeam(category.TeamID)
|
||||
ws.logger.Debug("listener(s) for teamID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", category.TeamID),
|
||||
mlog.String("categoryID", category.ID),
|
||||
)
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast block change",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", category.TeamID),
|
||||
mlog.String("categoryID", category.ID),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
if err := listener.WriteJSON(message); err != nil {
|
||||
ws.logger.Error("broadcast category change error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastCategoryReorder(teamID, userID string, categoryOrder []string) {
|
||||
message := CategoryReorderMessage{
|
||||
Action: websocketActionReorderCategories,
|
||||
CategoryOrder: categoryOrder,
|
||||
TeamID: teamID,
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForTeam(teamID)
|
||||
ws.logger.Debug("listener(s) for teamID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
)
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast category order change",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
if err := listener.WriteJSON(message); err != nil {
|
||||
ws.logger.Error("broadcast category order change error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastCategoryBoardsReorder(teamID, userID, categoryID string, boardOrder []string) {
|
||||
message := CategoryBoardReorderMessage{
|
||||
Action: websocketActionReorderCategoryBoards,
|
||||
CategoryID: categoryID,
|
||||
BoardOrder: boardOrder,
|
||||
TeamID: teamID,
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForTeam(teamID)
|
||||
ws.logger.Debug("listener(s) for teamID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
)
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast board category order change",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
if err := listener.WriteJSON(message); err != nil {
|
||||
ws.logger.Error("broadcast category order change error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastCategoryBoardChange(teamID, userID string, boardCategories []*model.BoardCategoryWebsocketData) {
|
||||
message := UpdateCategoryMessage{
|
||||
Action: websocketActionUpdateCategoryBoard,
|
||||
TeamID: teamID,
|
||||
BoardCategories: boardCategories,
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForTeam(teamID)
|
||||
ws.logger.Debug("listener(s) for teamID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.Int("numEntries", len(boardCategories)),
|
||||
)
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast block change",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.Int("numEntries", len(boardCategories)),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
if err := listener.WriteJSON(message); err != nil {
|
||||
ws.logger.Error("broadcast category change error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastConfigChange broadcasts update messages to clients.
|
||||
func (ws *Server) BroadcastConfigChange(clientConfig model.ClientConfig) {
|
||||
message := UpdateClientConfig{
|
||||
Action: websocketActionUpdateConfig,
|
||||
ClientConfig: clientConfig,
|
||||
}
|
||||
|
||||
listeners := ws.listeners
|
||||
ws.logger.Debug("broadcasting config change to listener(s)",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
)
|
||||
|
||||
for listener := range listeners {
|
||||
ws.logger.Debug("Broadcast Config change",
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
ws.logger.Error("broadcast error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastBoardChange(teamID string, board *model.Board) {
|
||||
message := UpdateBoardMsg{
|
||||
Action: websocketActionUpdateBoard,
|
||||
TeamID: teamID,
|
||||
Board: board,
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForTeamAndBoard(teamID, board.ID)
|
||||
ws.logger.Trace("listener(s) for teamID and boardID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", board.ID),
|
||||
)
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast board change",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", board.ID),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
ws.logger.Error("broadcast error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastBoardDelete(teamID, boardID string) {
|
||||
now := utils.GetMillis()
|
||||
board := &model.Board{}
|
||||
board.ID = boardID
|
||||
board.TeamID = teamID
|
||||
board.UpdateAt = now
|
||||
board.DeleteAt = now
|
||||
|
||||
ws.BroadcastBoardChange(teamID, board)
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastMemberChange(teamID, boardID string, member *model.BoardMember) {
|
||||
message := UpdateMemberMsg{
|
||||
Action: websocketActionUpdateMember,
|
||||
TeamID: teamID,
|
||||
Member: member,
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForTeamAndBoard(teamID, boardID)
|
||||
ws.logger.Trace("listener(s) for teamID and boardID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
)
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast member change",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
ws.logger.Error("broadcast error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastMemberDelete(teamID, boardID, userID string) {
|
||||
message := UpdateMemberMsg{
|
||||
Action: websocketActionDeleteMember,
|
||||
TeamID: teamID,
|
||||
Member: &model.BoardMember{UserID: userID, BoardID: boardID},
|
||||
}
|
||||
|
||||
// when fetching the members of the board that should receive the
|
||||
// member deletion message, the deleted member will not be one of
|
||||
// them, so we need to ensure they receive the message
|
||||
listeners := ws.getListenersForTeamAndBoard(teamID, boardID, userID)
|
||||
ws.logger.Trace("listener(s) for teamID and boardID",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
)
|
||||
|
||||
for _, listener := range listeners {
|
||||
ws.logger.Debug("Broadcast member removal",
|
||||
mlog.String("teamID", teamID),
|
||||
mlog.String("boardID", boardID),
|
||||
mlog.Stringer("remoteAddr", listener.conn.RemoteAddr()),
|
||||
)
|
||||
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
ws.logger.Error("broadcast error", mlog.Err(err))
|
||||
listener.conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) {
|
||||
// not implemented for standalone server.
|
||||
}
|
||||
|
||||
func (ws *Server) BroadcastCardLimitTimestampChange(cardLimitTimestamp int64) {
|
||||
// not implemented for standalone server.
|
||||
}
|
||||
244
server/boards/ws/server_test.go
Обычный файл
244
server/boards/ws/server_test.go
Обычный файл
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package ws
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/auth"
|
||||
"github.com/mattermost/mattermost-server/v6/server/boards/model"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTeamSubscription(t *testing.T) {
|
||||
server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil)
|
||||
session := &websocketSession{
|
||||
conn: &websocket.Conn{},
|
||||
mu: sync.Mutex{},
|
||||
teams: []string{},
|
||||
blocks: []string{},
|
||||
}
|
||||
teamID := "fake-team-id"
|
||||
|
||||
t.Run("Should correctly add a session", func(t *testing.T) {
|
||||
server.addListener(session)
|
||||
require.Len(t, server.listeners, 1)
|
||||
require.Empty(t, server.listenersByTeam)
|
||||
require.Empty(t, session.teams)
|
||||
})
|
||||
|
||||
t.Run("Should correctly subscribe to a team", func(t *testing.T) {
|
||||
require.False(t, session.isSubscribedToTeam(teamID))
|
||||
|
||||
server.subscribeListenerToTeam(session, teamID)
|
||||
|
||||
require.Len(t, server.listenersByTeam[teamID], 1)
|
||||
require.Contains(t, server.listenersByTeam[teamID], session)
|
||||
require.Len(t, session.teams, 1)
|
||||
require.Contains(t, session.teams, teamID)
|
||||
|
||||
require.True(t, session.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Subscribing again to a subscribed team would have no effect", func(t *testing.T) {
|
||||
require.True(t, session.isSubscribedToTeam(teamID))
|
||||
|
||||
server.subscribeListenerToTeam(session, teamID)
|
||||
|
||||
require.Len(t, server.listenersByTeam[teamID], 1)
|
||||
require.Contains(t, server.listenersByTeam[teamID], session)
|
||||
require.Len(t, session.teams, 1)
|
||||
require.Contains(t, session.teams, teamID)
|
||||
|
||||
require.True(t, session.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Should correctly unsubscribe to a team", func(t *testing.T) {
|
||||
require.True(t, session.isSubscribedToTeam(teamID))
|
||||
|
||||
server.unsubscribeListenerFromTeam(session, teamID)
|
||||
|
||||
require.Empty(t, server.listenersByTeam[teamID])
|
||||
require.Empty(t, session.teams)
|
||||
|
||||
require.False(t, session.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Unsubscribing again to an unsubscribed team would have no effect", func(t *testing.T) {
|
||||
require.False(t, session.isSubscribedToTeam(teamID))
|
||||
|
||||
server.unsubscribeListenerFromTeam(session, teamID)
|
||||
|
||||
require.Empty(t, server.listenersByTeam[teamID])
|
||||
require.Empty(t, session.teams)
|
||||
|
||||
require.False(t, session.isSubscribedToTeam(teamID))
|
||||
})
|
||||
|
||||
t.Run("Should correctly be removed from the server", func(t *testing.T) {
|
||||
server.removeListener(session)
|
||||
|
||||
require.Empty(t, server.listeners)
|
||||
})
|
||||
|
||||
t.Run("If subscribed to teams and removed, should be removed from the teams subscription list", func(t *testing.T) {
|
||||
teamID2 := "other-fake-team-id"
|
||||
|
||||
server.addListener(session)
|
||||
server.subscribeListenerToTeam(session, teamID)
|
||||
server.subscribeListenerToTeam(session, teamID2)
|
||||
|
||||
require.Len(t, server.listeners, 1)
|
||||
require.Contains(t, server.listenersByTeam[teamID], session)
|
||||
require.Contains(t, server.listenersByTeam[teamID2], session)
|
||||
|
||||
server.removeListener(session)
|
||||
|
||||
require.Empty(t, server.listeners)
|
||||
require.Empty(t, server.listenersByTeam[teamID])
|
||||
require.Empty(t, server.listenersByTeam[teamID2])
|
||||
})
|
||||
}
|
||||
|
||||
func TestBlocksSubscription(t *testing.T) {
|
||||
server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil)
|
||||
session := &websocketSession{
|
||||
conn: &websocket.Conn{},
|
||||
mu: sync.Mutex{},
|
||||
teams: []string{},
|
||||
blocks: []string{},
|
||||
}
|
||||
blockID1 := "block1"
|
||||
blockID2 := "block2"
|
||||
blockID3 := "block3"
|
||||
blockIDs := []string{blockID1, blockID2, blockID3}
|
||||
|
||||
t.Run("Should correctly add a session", func(t *testing.T) {
|
||||
server.addListener(session)
|
||||
require.Len(t, server.listeners, 1)
|
||||
require.Empty(t, server.listenersByTeam)
|
||||
require.Empty(t, session.teams)
|
||||
})
|
||||
|
||||
t.Run("Should correctly subscribe to a set of blocks", func(t *testing.T) {
|
||||
require.False(t, session.isSubscribedToBlock(blockID1))
|
||||
require.False(t, session.isSubscribedToBlock(blockID2))
|
||||
require.False(t, session.isSubscribedToBlock(blockID3))
|
||||
|
||||
server.subscribeListenerToBlocks(session, blockIDs)
|
||||
|
||||
require.Len(t, server.listenersByBlock[blockID1], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID1], session)
|
||||
require.Len(t, server.listenersByBlock[blockID2], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID2], session)
|
||||
require.Len(t, server.listenersByBlock[blockID3], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID3], session)
|
||||
require.Len(t, session.blocks, 3)
|
||||
require.ElementsMatch(t, blockIDs, session.blocks)
|
||||
|
||||
require.True(t, session.isSubscribedToBlock(blockID1))
|
||||
require.True(t, session.isSubscribedToBlock(blockID2))
|
||||
require.True(t, session.isSubscribedToBlock(blockID3))
|
||||
|
||||
t.Run("Subscribing again to a subscribed block would have no effect", func(t *testing.T) {
|
||||
require.True(t, session.isSubscribedToBlock(blockID1))
|
||||
require.True(t, session.isSubscribedToBlock(blockID2))
|
||||
require.True(t, session.isSubscribedToBlock(blockID3))
|
||||
|
||||
server.subscribeListenerToBlocks(session, blockIDs)
|
||||
|
||||
require.Len(t, server.listenersByBlock[blockID1], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID1], session)
|
||||
require.Len(t, server.listenersByBlock[blockID2], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID2], session)
|
||||
require.Len(t, server.listenersByBlock[blockID3], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID3], session)
|
||||
require.Len(t, session.blocks, 3)
|
||||
require.ElementsMatch(t, blockIDs, session.blocks)
|
||||
|
||||
require.True(t, session.isSubscribedToBlock(blockID1))
|
||||
require.True(t, session.isSubscribedToBlock(blockID2))
|
||||
require.True(t, session.isSubscribedToBlock(blockID3))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Should correctly unsubscribe to a set of blocks", func(t *testing.T) {
|
||||
require.True(t, session.isSubscribedToBlock(blockID1))
|
||||
require.True(t, session.isSubscribedToBlock(blockID2))
|
||||
require.True(t, session.isSubscribedToBlock(blockID3))
|
||||
|
||||
server.unsubscribeListenerFromBlocks(session, blockIDs)
|
||||
|
||||
require.Empty(t, server.listenersByBlock[blockID1])
|
||||
require.Empty(t, server.listenersByBlock[blockID2])
|
||||
require.Empty(t, server.listenersByBlock[blockID3])
|
||||
require.Empty(t, session.blocks)
|
||||
|
||||
require.False(t, session.isSubscribedToBlock(blockID1))
|
||||
require.False(t, session.isSubscribedToBlock(blockID2))
|
||||
require.False(t, session.isSubscribedToBlock(blockID3))
|
||||
})
|
||||
|
||||
t.Run("Unsubscribing again to an unsubscribed block would have no effect", func(t *testing.T) {
|
||||
require.False(t, session.isSubscribedToBlock(blockID1))
|
||||
|
||||
server.unsubscribeListenerFromBlocks(session, []string{blockID1})
|
||||
|
||||
require.Empty(t, server.listenersByBlock[blockID1])
|
||||
require.Empty(t, session.blocks)
|
||||
|
||||
require.False(t, session.isSubscribedToBlock(blockID1))
|
||||
})
|
||||
|
||||
t.Run("Should correctly be removed from the server", func(t *testing.T) {
|
||||
server.removeListener(session)
|
||||
|
||||
require.Empty(t, server.listeners)
|
||||
})
|
||||
|
||||
t.Run("If subscribed to blocks and removed, should be removed from the blocks subscription list", func(t *testing.T) {
|
||||
server.addListener(session)
|
||||
server.subscribeListenerToBlocks(session, blockIDs)
|
||||
|
||||
require.Len(t, server.listeners, 1)
|
||||
require.Len(t, server.listenersByBlock[blockID1], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID1], session)
|
||||
require.Len(t, server.listenersByBlock[blockID2], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID2], session)
|
||||
require.Len(t, server.listenersByBlock[blockID3], 1)
|
||||
require.Contains(t, server.listenersByBlock[blockID3], session)
|
||||
require.Len(t, session.blocks, 3)
|
||||
require.ElementsMatch(t, blockIDs, session.blocks)
|
||||
|
||||
server.removeListener(session)
|
||||
|
||||
require.Empty(t, server.listeners)
|
||||
require.Empty(t, server.listenersByBlock[blockID1])
|
||||
require.Empty(t, server.listenersByBlock[blockID2])
|
||||
require.Empty(t, server.listenersByBlock[blockID3])
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetUserIDForTokenInSingleUserMode(t *testing.T) {
|
||||
singleUserToken := "single-user-token"
|
||||
server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{}, nil)
|
||||
server.singleUserToken = singleUserToken
|
||||
|
||||
t.Run("Should return nothing if the token is empty", func(t *testing.T) {
|
||||
require.Empty(t, server.getUserIDForToken(""))
|
||||
})
|
||||
|
||||
t.Run("Should return nothing if the token is invalid", func(t *testing.T) {
|
||||
require.Empty(t, server.getUserIDForToken("invalid-token"))
|
||||
})
|
||||
|
||||
t.Run("Should return the single user ID if the token is correct", func(t *testing.T) {
|
||||
require.Equal(t, model.SingleUser, server.getUserIDForToken(singleUserToken))
|
||||
})
|
||||
}
|
||||
Ссылка в новой задаче
Block a user