Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

138
server/boards/model/auth.go Обычный файл
Просмотреть файл

@@ -0,0 +1,138 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/services/auth"
)
const (
MinimumPasswordLength = 8
)
func NewErrAuthParam(msg string) *ErrAuthParam {
return &ErrAuthParam{
msg: msg,
}
}
type ErrAuthParam struct {
msg string
}
func (pe *ErrAuthParam) Error() string {
return pe.msg
}
// LoginRequest is a login request
// swagger:model
type LoginRequest struct {
// Type of login, currently must be set to "normal"
// required: true
Type string `json:"type"`
// If specified, login using username
// required: false
Username string `json:"username"`
// If specified, login using email
// required: false
Email string `json:"email"`
// Password
// required: true
Password string `json:"password"`
// MFA token
// required: false
// swagger:ignore
MfaToken string `json:"mfa_token"`
}
// LoginResponse is a login response
// swagger:model
type LoginResponse struct {
// Session token
// required: true
Token string `json:"token"`
}
func LoginResponseFromJSON(data io.Reader) (*LoginResponse, error) {
var resp LoginResponse
if err := json.NewDecoder(data).Decode(&resp); err != nil {
return nil, err
}
return &resp, nil
}
// RegisterRequest is a user registration request
// swagger:model
type RegisterRequest struct {
// User name
// required: true
Username string `json:"username"`
// User's email
// required: true
Email string `json:"email"`
// Password
// required: true
Password string `json:"password"`
// Registration authorization token
// required: true
Token string `json:"token"`
}
func (rd *RegisterRequest) IsValid() error {
if strings.TrimSpace(rd.Username) == "" {
return NewErrAuthParam("username is required")
}
if strings.TrimSpace(rd.Email) == "" {
return NewErrAuthParam("email is required")
}
if !auth.IsEmailValid(rd.Email) {
return NewErrAuthParam("invalid email format")
}
if rd.Password == "" {
return NewErrAuthParam("password is required")
}
return isValidPassword(rd.Password)
}
// ChangePasswordRequest is a user password change request
// swagger:model
type ChangePasswordRequest struct {
// Old password
// required: true
OldPassword string `json:"oldPassword"`
// New password
// required: true
NewPassword string `json:"newPassword"`
}
// IsValid validates a password change request.
func (rd *ChangePasswordRequest) IsValid() error {
if rd.OldPassword == "" {
return NewErrAuthParam("old password is required")
}
if rd.NewPassword == "" {
return NewErrAuthParam("new password is required")
}
return isValidPassword(rd.NewPassword)
}
func isValidPassword(password string) error {
if len(password) < MinimumPasswordLength {
return NewErrAuthParam(fmt.Sprintf("password must be at least %d characters", MinimumPasswordLength))
}
return nil
}

258
server/boards/model/block.go Обычный файл
Просмотреть файл

@@ -0,0 +1,258 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"strconv"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
// Block is the basic data unit
// swagger:model
type Block struct {
// The id for this block
// required: true
ID string `json:"id"`
// The id for this block's parent block. Empty for root blocks
// required: false
ParentID string `json:"parentId"`
// The id for user who created this block
// required: true
CreatedBy string `json:"createdBy"`
// The id for user who last modified this block
// required: true
ModifiedBy string `json:"modifiedBy"`
// The schema version of this block
// required: true
Schema int64 `json:"schema"`
// The block type
// required: true
Type BlockType `json:"type"`
// The display title
// required: false
Title string `json:"title"`
// The block fields
// required: false
Fields map[string]interface{} `json:"fields"`
// The creation time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// The last modified time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
// The deleted time in miliseconds since the current epoch. Set to indicate this block is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
// Deprecated. The workspace id that the block belongs to
// required: false
WorkspaceID string `json:"-"`
// The board id that the block belongs to
// required: true
BoardID string `json:"boardId"`
// Indicates if the card is limited
// required: false
Limited bool `json:"limited,omitempty"`
}
// BlockPatch is a patch for modify blocks
// swagger:model
type BlockPatch struct {
// The id for this block's parent block. Empty for root blocks
// required: false
ParentID *string `json:"parentId"`
// The schema version of this block
// required: false
Schema *int64 `json:"schema"`
// The block type
// required: false
Type *BlockType `json:"type"`
// The display title
// required: false
Title *string `json:"title"`
// The block updated fields
// required: false
UpdatedFields map[string]interface{} `json:"updatedFields"`
// The block removed fields
// required: false
DeletedFields []string `json:"deletedFields"`
}
// BlockPatchBatch is a batch of IDs and patches for modify blocks
// swagger:model
type BlockPatchBatch struct {
// The id's for of the blocks to patch
BlockIDs []string `json:"block_ids"`
// The BlockPatches to be applied
BlockPatches []BlockPatch `json:"block_patches"`
}
// BoardModifier is a callback that can modify each board during an import.
// A cache of arbitrary data will be passed for each call and any changes
// to the cache will be preserved for the next call.
// Return true to import the block or false to skip import.
type BoardModifier func(board *Board, cache map[string]interface{}) bool
// BlockModifier is a callback that can modify each block during an import.
// A cache of arbitrary data will be passed for each call and any changes
// to the cache will be preserved for the next call.
// Return true to import the block or false to skip import.
type BlockModifier func(block *Block, cache map[string]interface{}) bool
func BlocksFromJSON(data io.Reader) []*Block {
var blocks []*Block
_ = json.NewDecoder(data).Decode(&blocks)
return blocks
}
// LogClone implements the `mlog.LogCloner` interface to provide a subset of Block fields for logging.
func (b *Block) LogClone() interface{} {
return struct {
ID string
ParentID string
BoardID string
Type BlockType
}{
ID: b.ID,
ParentID: b.ParentID,
BoardID: b.BoardID,
Type: b.Type,
}
}
// Patch returns an update version of the block.
func (p *BlockPatch) Patch(block *Block) *Block {
if p.ParentID != nil {
block.ParentID = *p.ParentID
}
if p.Schema != nil {
block.Schema = *p.Schema
}
if p.Type != nil {
block.Type = *p.Type
}
if p.Title != nil {
block.Title = *p.Title
}
for key, field := range p.UpdatedFields {
block.Fields[key] = field
}
for _, key := range p.DeletedFields {
delete(block.Fields, key)
}
return block
}
type QueryBlocksOptions struct {
BoardID string // if not empty then filter for blocks belonging to specified board
ParentID string // if not empty then filter for blocks belonging to specified parent
BlockType BlockType // if not empty and not `TypeUnknown` then filter for records of specified block type
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=-1, meaning unlimited)
}
// QuerySubtreeOptions are query options that can be passed to GetSubTree methods.
type QuerySubtreeOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Limit uint64 // if non-zero then limit the number of returned records
}
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
type QueryBlockHistoryOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Limit uint64 // if non-zero then limit the number of returned records
Descending bool // if true then the records are sorted by insert_at in descending order
}
// QueryBoardHistoryOptions are query options that can be passed to GetBoardHistory.
type QueryBoardHistoryOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Limit uint64 // if non-zero then limit the number of returned records
Descending bool // if true then the records are sorted by insert_at in descending order
}
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
type QueryBlockHistoryChildOptions struct {
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=-1, meaning unlimited)
}
func StampModificationMetadata(userID string, blocks []*Block, auditRec *audit.Record) {
if userID == SingleUser {
userID = ""
}
now := GetMillis()
for i := range blocks {
blocks[i].ModifiedBy = userID
blocks[i].UpdateAt = now
if auditRec != nil {
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i])
}
}
}
func (b *Block) ShouldBeLimited(cardLimitTimestamp int64) bool {
return b.Type == TypeCard &&
b.UpdateAt < cardLimitTimestamp
}
// Returns a limited version of the block that doesn't contain the
// contents of the block, only its IDs and type.
func (b *Block) GetLimited() *Block {
newBlock := &Block{
Title: b.Title,
ID: b.ID,
ParentID: b.ParentID,
BoardID: b.BoardID,
Schema: b.Schema,
Type: b.Type,
CreateAt: b.CreateAt,
UpdateAt: b.UpdateAt,
DeleteAt: b.DeleteAt,
WorkspaceID: b.WorkspaceID,
Limited: true,
}
if iconField, ok := b.Fields["icon"]; ok {
newBlock.Fields = map[string]interface{}{
"icon": iconField,
}
}
return newBlock
}

312
server/boards/model/block_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,312 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/stretchr/testify/require"
)
func TestGenerateBlockIDs(t *testing.T) {
t.Run("Should generate a new ID for a single block with no references", func(t *testing.T) {
blockID := utils.NewID(utils.IDTypeBlock)
blocks := []*Block{{ID: blockID}}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID, blocks[0].ID)
require.Zero(t, blocks[0].BoardID)
require.Zero(t, blocks[0].ParentID)
})
t.Run("Should generate a new ID for a single block with references", func(t *testing.T) {
blockID := utils.NewID(utils.IDTypeBlock)
boardID := utils.NewID(utils.IDTypeBlock)
parentID := utils.NewID(utils.IDTypeBlock)
blocks := []*Block{{ID: blockID, BoardID: boardID, ParentID: parentID}}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID, blocks[0].ID)
require.Equal(t, boardID, blocks[0].BoardID)
require.Equal(t, parentID, blocks[0].ParentID)
})
t.Run("Should generate IDs and link multiple blocks with existing references", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := &Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
blockID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := blockID1
parentID2 := utils.NewID(utils.IDTypeBlock)
block2 := &Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
blocks := []*Block{block1, block2}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID1, blocks[0].ID)
require.Equal(t, boardID1, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID2, blocks[1].ID)
require.NotEqual(t, boardID2, blocks[1].BoardID)
require.Equal(t, parentID2, blocks[1].ParentID)
// blockID1 was referenced, so it should still be after the ID
// changes
require.Equal(t, blocks[0].ID, blocks[1].BoardID)
})
t.Run("Should generate new IDs but not modify nonexisting references", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := ""
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := &Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
blockID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := utils.NewID(utils.IDTypeBlock)
parentID2 := ""
block2 := &Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
blocks := []*Block{block1, block2}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
// only the IDs should have changed
require.NotEqual(t, blockID1, blocks[0].ID)
require.Zero(t, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID2, blocks[1].ID)
require.Equal(t, boardID2, blocks[1].BoardID)
require.Zero(t, blocks[1].ParentID)
})
t.Run("Should modify correctly multiple blocks with existing and nonexisting references", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := &Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1}
// linked to 1
blockID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := blockID1
parentID2 := utils.NewID(utils.IDTypeBlock)
block2 := &Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2}
// linked to 2
blockID3 := utils.NewID(utils.IDTypeBlock)
boardID3 := blockID2
parentID3 := utils.NewID(utils.IDTypeBlock)
block3 := &Block{ID: blockID3, BoardID: boardID3, ParentID: parentID3}
// linked to 1
blockID4 := utils.NewID(utils.IDTypeBlock)
boardID4 := blockID1
parentID4 := utils.NewID(utils.IDTypeBlock)
block4 := &Block{ID: blockID4, BoardID: boardID4, ParentID: parentID4}
// blocks are shuffled
blocks := []*Block{block4, block2, block1, block3}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
// block 1
require.NotEqual(t, blockID1, blocks[2].ID)
require.Equal(t, boardID1, blocks[2].BoardID)
require.Equal(t, parentID1, blocks[2].ParentID)
// block 2
require.NotEqual(t, blockID2, blocks[1].ID)
require.NotEqual(t, boardID2, blocks[1].BoardID)
require.Equal(t, blocks[2].ID, blocks[1].BoardID) // link to 1
require.Equal(t, parentID2, blocks[1].ParentID)
// block 3
require.NotEqual(t, blockID3, blocks[3].ID)
require.NotEqual(t, boardID3, blocks[3].BoardID)
require.Equal(t, blocks[1].ID, blocks[3].BoardID) // link to 2
require.Equal(t, parentID3, blocks[3].ParentID)
// block 4
require.NotEqual(t, blockID4, blocks[0].ID)
require.NotEqual(t, boardID4, blocks[0].BoardID)
require.Equal(t, blocks[2].ID, blocks[0].BoardID) // link to 1
require.Equal(t, parentID4, blocks[0].ParentID)
})
t.Run("Should update content order", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := &Block{
ID: blockID1,
BoardID: boardID1,
ParentID: parentID1,
}
blockID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := utils.NewID(utils.IDTypeBlock)
parentID2 := utils.NewID(utils.IDTypeBlock)
block2 := &Block{
ID: blockID2,
BoardID: boardID2,
ParentID: parentID2,
Fields: map[string]interface{}{
"contentOrder": []interface{}{
blockID1,
},
},
}
blocks := []*Block{block1, block2}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID1, blocks[0].ID)
require.Equal(t, boardID1, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID2, blocks[1].ID)
require.Equal(t, boardID2, blocks[1].BoardID)
require.Equal(t, parentID2, blocks[1].ParentID)
// since block 1 was referenced in block 2,
// the ID should have been changed in content order
block2ContentOrder, ok := block2.Fields["contentOrder"].([]interface{})
require.True(t, ok)
require.NotEqual(t, blockID1, block2ContentOrder[0].(string))
require.Equal(t, blocks[0].ID, block2ContentOrder[0].(string))
})
t.Run("Should update content order when it contain slices", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := &Block{
ID: blockID1,
BoardID: boardID1,
ParentID: parentID1,
}
blockID2 := utils.NewID(utils.IDTypeBlock)
block2 := &Block{
ID: blockID2,
BoardID: boardID1,
ParentID: parentID1,
}
blockID3 := utils.NewID(utils.IDTypeBlock)
block3 := &Block{
ID: blockID3,
BoardID: boardID1,
ParentID: parentID1,
}
blockID4 := utils.NewID(utils.IDTypeBlock)
boardID2 := utils.NewID(utils.IDTypeBlock)
parentID2 := utils.NewID(utils.IDTypeBlock)
block4 := &Block{
ID: blockID4,
BoardID: boardID2,
ParentID: parentID2,
Fields: map[string]interface{}{
"contentOrder": []interface{}{
blockID1,
[]interface{}{
blockID2,
blockID3,
},
},
},
}
blocks := []*Block{block1, block2, block3, block4}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID1, blocks[0].ID)
require.Equal(t, boardID1, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID4, blocks[3].ID)
require.Equal(t, boardID2, blocks[3].BoardID)
require.Equal(t, parentID2, blocks[3].ParentID)
// since block 1 was referenced in block 2,
// the ID should have been changed in content order
block4ContentOrder, ok := block4.Fields["contentOrder"].([]interface{})
require.True(t, ok)
require.NotEqual(t, blockID1, block4ContentOrder[0].(string))
require.NotEqual(t, blockID2, block4ContentOrder[1].([]interface{})[0])
require.NotEqual(t, blockID3, block4ContentOrder[1].([]interface{})[1])
require.Equal(t, blocks[0].ID, block4ContentOrder[0].(string))
require.Equal(t, blocks[1].ID, block4ContentOrder[1].([]interface{})[0])
require.Equal(t, blocks[2].ID, block4ContentOrder[1].([]interface{})[1])
})
t.Run("Should update Id of default template view", func(t *testing.T) {
blockID1 := utils.NewID(utils.IDTypeBlock)
boardID1 := utils.NewID(utils.IDTypeBlock)
parentID1 := utils.NewID(utils.IDTypeBlock)
block1 := &Block{
ID: blockID1,
BoardID: boardID1,
ParentID: parentID1,
}
blockID2 := utils.NewID(utils.IDTypeBlock)
boardID2 := utils.NewID(utils.IDTypeBlock)
parentID2 := utils.NewID(utils.IDTypeBlock)
block2 := &Block{
ID: blockID2,
BoardID: boardID2,
ParentID: parentID2,
Fields: map[string]interface{}{
"defaultTemplateId": blockID1,
},
}
blocks := []*Block{block1, block2}
blocks = GenerateBlockIDs(blocks, &mlog.Logger{})
require.NotEqual(t, blockID1, blocks[0].ID)
require.Equal(t, boardID1, blocks[0].BoardID)
require.Equal(t, parentID1, blocks[0].ParentID)
require.NotEqual(t, blockID2, blocks[1].ID)
require.Equal(t, boardID2, blocks[1].BoardID)
require.Equal(t, parentID2, blocks[1].ParentID)
block2DefaultTemplateID, ok := block2.Fields["defaultTemplateId"].(string)
require.True(t, ok)
require.NotEqual(t, blockID1, block2DefaultTemplateID)
require.Equal(t, blocks[0].ID, block2DefaultTemplateID)
})
}
func TestStampModificationMetadata(t *testing.T) {
t.Run("base case", func(t *testing.T) {
block := &Block{}
blocks := []*Block{block}
assert.Empty(t, block.ModifiedBy)
assert.Empty(t, block.UpdateAt)
StampModificationMetadata("user_id_1", blocks, nil)
assert.Equal(t, "user_id_1", blocks[0].ModifiedBy)
assert.NotEmpty(t, blocks[0].UpdateAt)
})
}

159
server/boards/model/blockid.go Обычный файл
Просмотреть файл

@@ -0,0 +1,159 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"fmt"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// GenerateBlockIDs generates new IDs for all the blocks of the list,
// keeping consistent any references that other blocks would made to
// the original IDs, so a tree of blocks can get new IDs and maintain
// its shape.
func GenerateBlockIDs(blocks []*Block, logger mlog.LoggerIFace) []*Block {
blockIDs := map[string]BlockType{}
referenceIDs := map[string]bool{}
for _, block := range blocks {
if _, ok := blockIDs[block.ID]; !ok {
blockIDs[block.ID] = block.Type
}
if _, ok := referenceIDs[block.BoardID]; !ok {
referenceIDs[block.BoardID] = true
}
if _, ok := referenceIDs[block.ParentID]; !ok {
referenceIDs[block.ParentID] = true
}
if _, ok := block.Fields["contentOrder"]; ok {
contentOrder, typeOk := block.Fields["contentOrder"].([]interface{})
if !typeOk {
logger.Warn(
"type assertion failed for content order when saving reference block IDs",
mlog.String("blockID", block.ID),
mlog.String("actionType", fmt.Sprintf("%T", block.Fields["contentOrder"])),
mlog.String("expectedType", "[]interface{}"),
mlog.String("contentOrder", fmt.Sprintf("%v", block.Fields["contentOrder"])),
)
continue
}
for _, blockID := range contentOrder {
switch v := blockID.(type) {
case []interface{}:
for _, columnBlockID := range v {
referenceIDs[columnBlockID.(string)] = true
}
case string:
referenceIDs[v] = true
default:
}
}
}
if _, ok := block.Fields["defaultTemplateId"]; ok {
defaultTemplateID, typeOk := block.Fields["defaultTemplateId"].(string)
if !typeOk {
logger.Warn(
"type assertion failed for default template ID when saving reference block IDs",
mlog.String("blockID", block.ID),
mlog.String("actionType", fmt.Sprintf("%T", block.Fields["defaultTemplateId"])),
mlog.String("expectedType", "string"),
mlog.String("defaultTemplateId", fmt.Sprintf("%v", block.Fields["defaultTemplateId"])),
)
continue
}
referenceIDs[defaultTemplateID] = true
}
}
newIDs := map[string]string{}
for id, blockType := range blockIDs {
for referenceID := range referenceIDs {
if id == referenceID {
newIDs[id] = utils.NewID(BlockType2IDType(blockType))
continue
}
}
}
getExistingOrOldID := func(id string) string {
if existingID, ok := newIDs[id]; ok {
return existingID
}
return id
}
getExistingOrNewID := func(id string) string {
if existingID, ok := newIDs[id]; ok {
return existingID
}
return utils.NewID(BlockType2IDType(blockIDs[id]))
}
newBlocks := make([]*Block, len(blocks))
for i, block := range blocks {
block.ID = getExistingOrNewID(block.ID)
block.BoardID = getExistingOrOldID(block.BoardID)
block.ParentID = getExistingOrOldID(block.ParentID)
blockMod := block
if _, ok := blockMod.Fields["contentOrder"]; ok {
fixFieldIDs(blockMod, "contentOrder", getExistingOrOldID, logger)
}
if _, ok := blockMod.Fields["cardOrder"]; ok {
fixFieldIDs(blockMod, "cardOrder", getExistingOrOldID, logger)
}
if _, ok := blockMod.Fields["defaultTemplateId"]; ok {
defaultTemplateID, typeOk := blockMod.Fields["defaultTemplateId"].(string)
if !typeOk {
logger.Warn(
"type assertion failed for default template ID when saving reference block IDs",
mlog.String("blockID", blockMod.ID),
mlog.String("actionType", fmt.Sprintf("%T", blockMod.Fields["defaultTemplateId"])),
mlog.String("expectedType", "string"),
mlog.String("defaultTemplateId", fmt.Sprintf("%v", blockMod.Fields["defaultTemplateId"])),
)
} else {
blockMod.Fields["defaultTemplateId"] = getExistingOrOldID(defaultTemplateID)
}
}
newBlocks[i] = blockMod
}
return newBlocks
}
func fixFieldIDs(block *Block, fieldName string, getExistingOrOldID func(string) string, logger mlog.LoggerIFace) {
field, typeOk := block.Fields[fieldName].([]interface{})
if !typeOk {
logger.Warn(
"type assertion failed for JSON field when setting new block IDs",
mlog.String("blockID", block.ID),
mlog.String("fieldName", fieldName),
mlog.String("actionType", fmt.Sprintf("%T", block.Fields[fieldName])),
mlog.String("expectedType", "[]interface{}"),
mlog.String("value", fmt.Sprintf("%v", block.Fields[fieldName])),
)
} else {
for j := range field {
switch v := field[j].(type) {
case string:
field[j] = getExistingOrOldID(v)
case []interface{}:
subOrder := field[j].([]interface{})
for k := range v {
subOrder[k] = getExistingOrOldID(v[k].(string))
}
}
}
}
}

88
server/boards/model/blocktype.go Обычный файл
Просмотреть файл

@@ -0,0 +1,88 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"errors"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
// BlockType represents a block type.
type BlockType string
const (
TypeUnknown = "unknown"
TypeBoard = "board"
TypeCard = "card"
TypeView = "view"
TypeText = "text"
TypeCheckbox = "checkbox"
TypeComment = "comment"
TypeImage = "image"
TypeAttachment = "attachment"
TypeDivider = "divider"
)
func (bt BlockType) String() string {
return string(bt)
}
// BlockTypeFromString returns an appropriate BlockType for the specified string.
func BlockTypeFromString(s string) (BlockType, error) {
switch strings.ToLower(s) {
case "board":
return TypeBoard, nil
case "card":
return TypeCard, nil
case "view":
return TypeView, nil
case "text":
return TypeText, nil
case "checkbox":
return TypeCheckbox, nil
case "comment":
return TypeComment, nil
case "image":
return TypeImage, nil
case "attachment":
return TypeAttachment, nil
case "divider":
return TypeDivider, nil
}
return TypeUnknown, ErrInvalidBlockType{s}
}
// BlockType2IDType returns an appropriate IDType for the specified BlockType.
func BlockType2IDType(blockType BlockType) utils.IDType {
switch blockType {
case TypeBoard:
return utils.IDTypeBoard
case TypeCard:
return utils.IDTypeCard
case TypeView:
return utils.IDTypeView
case TypeText, TypeCheckbox, TypeComment, TypeDivider:
return utils.IDTypeBlock
case TypeImage, TypeAttachment:
return utils.IDTypeAttachment
}
return utils.IDTypeNone
}
// ErrInvalidBlockType is returned wherever an invalid block type was provided.
type ErrInvalidBlockType struct {
Type string
}
func (e ErrInvalidBlockType) Error() string {
return e.Type + " is an invalid block type."
}
// IsErrInvalidBlockType returns true if `err` is a IsErrInvalidBlockType or wraps one.
func IsErrInvalidBlockType(err error) bool {
var eibt *ErrInvalidBlockType
return errors.As(err, &eibt)
}

429
server/boards/model/board.go Обычный файл
Просмотреть файл

@@ -0,0 +1,429 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"time"
)
type BoardType string
type BoardRole string
type BoardSearchField string
const (
BoardTypeOpen BoardType = "O"
BoardTypePrivate BoardType = "P"
)
const (
BoardRoleNone BoardRole = ""
BoardRoleViewer BoardRole = "viewer"
BoardRoleCommenter BoardRole = "commenter"
BoardRoleEditor BoardRole = "editor"
BoardRoleAdmin BoardRole = "admin"
)
const (
BoardSearchFieldNone BoardSearchField = ""
BoardSearchFieldTitle BoardSearchField = "title"
BoardSearchFieldPropertyName BoardSearchField = "property_name"
)
// Board groups a set of blocks and its layout
// swagger:model
type Board struct {
// The ID for the board
// required: true
ID string `json:"id"`
// The ID of the team that the board belongs to
// required: true
TeamID string `json:"teamId"`
// The ID of the channel that the board was created from
// required: false
ChannelID string `json:"channelId"`
// The ID of the user that created the board
// required: true
CreatedBy string `json:"createdBy"`
// The ID of the last user that updated the board
// required: true
ModifiedBy string `json:"modifiedBy"`
// The type of the board
// required: true
Type BoardType `json:"type"`
// The minimum role applied when somebody joins the board
// required: true
MinimumRole BoardRole `json:"minimumRole"`
// The title of the board
// required: false
Title string `json:"title"`
// The description of the board
// required: false
Description string `json:"description"`
// The icon of the board
// required: false
Icon string `json:"icon"`
// Indicates if the board shows the description on the interface
// required: false
ShowDescription bool `json:"showDescription"`
// Marks the template boards
// required: false
IsTemplate bool `json:"isTemplate"`
// Marks the template boards
// required: false
TemplateVersion int `json:"templateVersion"`
// The properties of the board
// required: false
Properties map[string]interface{} `json:"properties"`
// The properties of the board cards
// required: false
CardProperties []map[string]interface{} `json:"cardProperties"`
// The creation time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// The last modified time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
// The deleted time in miliseconds since the current epoch. Set to indicate this block is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
}
// GetPropertyString returns the value of the specified property as a string,
// or error if the property does not exist or is not of type string.
func (b *Board) GetPropertyString(propName string) (string, error) {
val, ok := b.Properties[propName]
if !ok {
return "", NewErrNotFound(propName)
}
s, ok := val.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
return s, nil
}
// BoardPatch is a patch for modify boards
// swagger:model
type BoardPatch struct {
// The type of the board
// required: false
Type *BoardType `json:"type"`
// The minimum role applied when somebody joins the board
// required: false
MinimumRole *BoardRole `json:"minimumRole"`
// The title of the board
// required: false
Title *string `json:"title"`
// The description of the board
// required: false
Description *string `json:"description"`
// The icon of the board
// required: false
Icon *string `json:"icon"`
// Indicates if the board shows the description on the interface
// required: false
ShowDescription *bool `json:"showDescription"`
// Indicates if the board shows the description on the interface
// required: false
ChannelID *string `json:"channelId"`
// The board updated properties
// required: false
UpdatedProperties map[string]interface{} `json:"updatedProperties"`
// The board removed properties
// required: false
DeletedProperties []string `json:"deletedProperties"`
// The board updated card properties
// required: false
UpdatedCardProperties []map[string]interface{} `json:"updatedCardProperties"`
// The board removed card properties
// required: false
DeletedCardProperties []string `json:"deletedCardProperties"`
}
// BoardMember stores the information of the membership of a user on a board
// swagger:model
type BoardMember struct {
// The ID of the board
// required: true
BoardID string `json:"boardId"`
// The ID of the user
// required: true
UserID string `json:"userId"`
// The independent roles of the user on the board
// required: false
Roles string `json:"roles"`
// Minimum role because the board configuration
// required: false
MinimumRole string `json:"minimumRole"`
// Marks the user as an admin of the board
// required: true
SchemeAdmin bool `json:"schemeAdmin"`
// Marks the user as an editor of the board
// required: true
SchemeEditor bool `json:"schemeEditor"`
// Marks the user as an commenter of the board
// required: true
SchemeCommenter bool `json:"schemeCommenter"`
// Marks the user as an viewer of the board
// required: true
SchemeViewer bool `json:"schemeViewer"`
// Marks the membership as generated by an access group
// required: true
Synthetic bool `json:"synthetic"`
}
// BoardMetadata contains metadata for a Board
// swagger:model
type BoardMetadata struct {
// The ID for the board
// required: true
BoardID string `json:"boardId"`
// The most recent time a descendant of this board was added, modified, or deleted
// required: true
DescendantLastUpdateAt int64 `json:"descendantLastUpdateAt"`
// The earliest time a descendant of this board was added, modified, or deleted
// required: true
DescendantFirstUpdateAt int64 `json:"descendantFirstUpdateAt"`
// The ID of the user that created the board
// required: true
CreatedBy string `json:"createdBy"`
// The ID of the user that last modified the most recently modified descendant
// required: true
LastModifiedBy string `json:"lastModifiedBy"`
}
func BoardFromJSON(data io.Reader) *Board {
var board *Board
_ = json.NewDecoder(data).Decode(&board)
return board
}
func BoardsFromJSON(data io.Reader) []*Board {
var boards []*Board
_ = json.NewDecoder(data).Decode(&boards)
return boards
}
func BoardMemberFromJSON(data io.Reader) *BoardMember {
var boardMember *BoardMember
_ = json.NewDecoder(data).Decode(&boardMember)
return boardMember
}
func BoardMembersFromJSON(data io.Reader) []*BoardMember {
var boardMembers []*BoardMember
_ = json.NewDecoder(data).Decode(&boardMembers)
return boardMembers
}
func BoardMetadataFromJSON(data io.Reader) *BoardMetadata {
var boardMetadata *BoardMetadata
_ = json.NewDecoder(data).Decode(&boardMetadata)
return boardMetadata
}
// Patch returns an updated version of the board.
func (p *BoardPatch) Patch(board *Board) *Board {
if p.Type != nil {
board.Type = *p.Type
}
if p.Title != nil {
board.Title = *p.Title
}
if p.MinimumRole != nil {
board.MinimumRole = *p.MinimumRole
}
if p.Description != nil {
board.Description = *p.Description
}
if p.Icon != nil {
board.Icon = *p.Icon
}
if p.ShowDescription != nil {
board.ShowDescription = *p.ShowDescription
}
if p.ChannelID != nil {
board.ChannelID = *p.ChannelID
}
for key, property := range p.UpdatedProperties {
board.Properties[key] = property
}
for _, key := range p.DeletedProperties {
delete(board.Properties, key)
}
if len(p.UpdatedCardProperties) != 0 || len(p.DeletedCardProperties) != 0 {
// first we accumulate all properties indexed by, and maintain their order
keyOrder := []string{}
cardPropertyMap := map[string]map[string]interface{}{}
for _, prop := range board.CardProperties {
id, ok := prop["id"].(string)
if !ok {
// bad property, skipping
continue
}
cardPropertyMap[id] = prop
keyOrder = append(keyOrder, id)
}
// if there are properties marked for removal, we delete them
for _, propertyID := range p.DeletedCardProperties {
delete(cardPropertyMap, propertyID)
}
// if there are properties marked for update, we replace the
// existing ones or add them
for _, newprop := range p.UpdatedCardProperties {
id, ok := newprop["id"].(string)
if !ok {
// bad new property, skipping
continue
}
_, exists := cardPropertyMap[id]
if !exists {
keyOrder = append(keyOrder, id)
}
cardPropertyMap[id] = newprop
}
// and finally we flatten and save the updated properties
newCardProperties := []map[string]interface{}{}
for _, key := range keyOrder {
p, exists := cardPropertyMap[key]
if exists {
newCardProperties = append(newCardProperties, p)
}
}
board.CardProperties = newCardProperties
}
return board
}
func IsBoardTypeValid(t BoardType) bool {
return t == BoardTypeOpen || t == BoardTypePrivate
}
func IsBoardMinimumRoleValid(r BoardRole) bool {
return r == BoardRoleNone || r == BoardRoleAdmin || r == BoardRoleEditor || r == BoardRoleCommenter || r == BoardRoleViewer
}
func (p *BoardPatch) IsValid() error {
if p.Type != nil && !IsBoardTypeValid(*p.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
if p.MinimumRole != nil && !IsBoardMinimumRoleValid(*p.MinimumRole) {
return InvalidBoardErr{"invalid-board-minimum-role"}
}
return nil
}
type InvalidBoardErr struct {
msg string
}
func (ibe InvalidBoardErr) Error() string {
return ibe.msg
}
func (b *Board) IsValid() error {
if b.TeamID == "" {
return InvalidBoardErr{"empty-team-id"}
}
if !IsBoardTypeValid(b.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
if !IsBoardMinimumRoleValid(b.MinimumRole) {
return InvalidBoardErr{"invalid-board-minimum-role"}
}
return nil
}
// BoardMemberHistoryEntry stores the information of the membership of a user on a board
// swagger:model
type BoardMemberHistoryEntry struct {
// The ID of the board
// required: true
BoardID string `json:"boardId"`
// The ID of the user
// required: true
UserID string `json:"userId"`
// The action that added this history entry (created or deleted)
// required: false
Action string `json:"action"`
// The insertion time
// required: true
InsertAt time.Time `json:"insertAt"`
}
func BoardSearchFieldFromString(field string) (BoardSearchField, error) {
switch field {
case string(BoardSearchFieldTitle):
return BoardSearchFieldTitle, nil
case string(BoardSearchFieldPropertyName):
return BoardSearchFieldPropertyName, nil
}
return BoardSearchFieldNone, ErrInvalidBoardSearchField
}

65
server/boards/model/board_insights.go Обычный файл
Просмотреть файл

@@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
// BoardInsightsList is a response type with pagination support.
type BoardInsightsList struct {
mm_model.InsightsListData
Items []*BoardInsight `json:"items"`
}
// BoardInsight gives insight into activities in a Board
// swagger:model
type BoardInsight struct {
// ID of the board
// required: true
BoardID string `json:"boardID"`
// icon of the board
// required: false
Icon string `json:"icon"`
// Title of the board
// required: false
Title string `json:"title"`
// Metric of how active the board is
// required: true
ActivityCount string `json:"activityCount"`
// IDs of users active on the board
// required: true
ActiveUsers mm_model.StringArray `json:"activeUsers"`
// ID of user who created the board
// required: true
CreatedBy string `json:"createdBy"`
}
func BoardInsightsFromJSON(data io.Reader) []BoardInsight {
var boardInsights []BoardInsight
_ = json.NewDecoder(data).Decode(&boardInsights)
return boardInsights
}
// GetTopBoardInsightsListWithPagination adds a rank to each item in the given list of BoardInsight and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of BoardInsight is assumed to be
// sorted by ActivityCount(score). Returns a BoardInsightsList.
func GetTopBoardInsightsListWithPagination(boards []*BoardInsight, limit int) *BoardInsightsList {
// Add pagination support
var hasNext bool
if limit != 0 && len(boards) == limit+1 {
hasNext = true
boards = boards[:len(boards)-1]
}
return &BoardInsightsList{InsightsListData: mm_model.InsightsListData{HasNext: hasNext}, Items: boards}
}

15
server/boards/model/board_statistics.go Обычный файл
Просмотреть файл

@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// BoardsStatistics is the representation of the statistics for the Boards server
// swagger:model
type BoardsStatistics struct {
// The maximum number of cards on the server
// required: true
Boards int `json:"board_count"`
// The maximum number of cards on the server
// required: true
Cards int `json:"card_count"`
}

175
server/boards/model/boards_and_blocks.go Обычный файл
Просмотреть файл

@@ -0,0 +1,175 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"errors"
"fmt"
"io"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrNoBoardsInBoardsAndBlocks = errors.New("at least one board is required")
var ErrNoBlocksInBoardsAndBlocks = errors.New("at least one block is required")
var ErrNoTeamInBoardsAndBlocks = errors.New("team ID cannot be empty")
var ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("board ids and patches need to match")
var ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("block ids and patches need to match")
type BlockDoesntBelongToAnyBoardErr struct {
blockID string
}
func (e BlockDoesntBelongToAnyBoardErr) Error() string {
return fmt.Sprintf("block %s doesn't belong to any board", e.blockID)
}
// BoardsAndBlocks is used to operate over boards and blocks at the
// same time
// swagger:model
type BoardsAndBlocks struct {
// The boards
// required: false
Boards []*Board `json:"boards"`
// The blocks
// required: false
Blocks []*Block `json:"blocks"`
}
func (bab *BoardsAndBlocks) IsValid() error {
if len(bab.Boards) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
if len(bab.Blocks) == 0 {
return ErrNoBlocksInBoardsAndBlocks
}
boardsMap := map[string]bool{}
for _, board := range bab.Boards {
boardsMap[board.ID] = true
}
for _, block := range bab.Blocks {
if _, ok := boardsMap[block.BoardID]; !ok {
return BlockDoesntBelongToAnyBoardErr{block.ID}
}
}
return nil
}
// DeleteBoardsAndBlocks is used to list the boards and blocks to
// delete on a request
// swagger:model
type DeleteBoardsAndBlocks struct {
// The boards
// required: true
Boards []string `json:"boards"`
// The blocks
// required: true
Blocks []string `json:"blocks"`
}
func NewDeleteBoardsAndBlocksFromBabs(babs *BoardsAndBlocks) *DeleteBoardsAndBlocks {
boardIDs := make([]string, 0, len(babs.Boards))
blockIDs := make([]string, 0, len(babs.Boards))
for _, board := range babs.Boards {
boardIDs = append(boardIDs, board.ID)
}
for _, block := range babs.Blocks {
blockIDs = append(blockIDs, block.ID)
}
return &DeleteBoardsAndBlocks{
Boards: boardIDs,
Blocks: blockIDs,
}
}
func (dbab *DeleteBoardsAndBlocks) IsValid() error {
if len(dbab.Boards) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
return nil
}
// PatchBoardsAndBlocks is used to patch multiple boards and blocks on
// a single request
// swagger:model
type PatchBoardsAndBlocks struct {
// The board IDs to patch
// required: true
BoardIDs []string `json:"boardIDs"`
// The board patches
// required: true
BoardPatches []*BoardPatch `json:"boardPatches"`
// The block IDs to patch
// required: true
BlockIDs []string `json:"blockIDs"`
// The block patches
// required: true
BlockPatches []*BlockPatch `json:"blockPatches"`
}
func (dbab *PatchBoardsAndBlocks) IsValid() error {
if len(dbab.BoardIDs) == 0 {
return ErrNoBoardsInBoardsAndBlocks
}
if len(dbab.BoardIDs) != len(dbab.BoardPatches) {
return ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks
}
if len(dbab.BlockIDs) != len(dbab.BlockPatches) {
return ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks
}
return nil
}
func GenerateBoardsAndBlocksIDs(bab *BoardsAndBlocks, logger mlog.LoggerIFace) (*BoardsAndBlocks, error) {
if err := bab.IsValid(); err != nil {
return nil, err
}
blocksByBoard := map[string][]*Block{}
for _, block := range bab.Blocks {
blocksByBoard[block.BoardID] = append(blocksByBoard[block.BoardID], block)
}
boards := []*Board{}
blocks := []*Block{}
for _, board := range bab.Boards {
newID := utils.NewID(utils.IDTypeBoard)
for _, block := range blocksByBoard[board.ID] {
block.BoardID = newID
blocks = append(blocks, block)
}
board.ID = newID
boards = append(boards, board)
}
newBab := &BoardsAndBlocks{
Boards: boards,
Blocks: GenerateBlockIDs(blocks, logger),
}
return newBab, nil
}
func BoardsAndBlocksFromJSON(data io.Reader) *BoardsAndBlocks {
var bab *BoardsAndBlocks
_ = json.NewDecoder(data).Decode(&bab)
return bab
}

254
server/boards/model/boards_and_blocks_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,254 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func TestIsValidBoardsAndBlocks(t *testing.T) {
t.Run("no boards", func(t *testing.T) {
bab := &BoardsAndBlocks{
Blocks: []*Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
require.ErrorIs(t, bab.IsValid(), ErrNoBoardsInBoardsAndBlocks)
})
t.Run("no blocks", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen},
{ID: "board-id-2", Type: BoardTypePrivate},
},
}
require.ErrorIs(t, bab.IsValid(), ErrNoBlocksInBoardsAndBlocks)
})
t.Run("block that doesn't belong to the boards", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen},
{ID: "board-id-2", Type: BoardTypePrivate},
},
Blocks: []*Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-3", BoardID: "board-id-3", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
require.ErrorIs(t, bab.IsValid(), BlockDoesntBelongToAnyBoardErr{"block-id-3"})
})
t.Run("valid boards and blocks", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen},
{ID: "board-id-2", Type: BoardTypePrivate},
},
Blocks: []*Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-3", BoardID: "board-id-2", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
require.NoError(t, bab.IsValid())
})
}
func TestGenerateBoardsAndBlocksIDs(t *testing.T) {
logger, err := mlog.NewLogger()
require.NoError(t, err)
getBlockByType := func(blocks []*Block, blockType BlockType) *Block {
for _, b := range blocks {
if b.Type == blockType {
return b
}
}
return &Block{}
}
getBoardByTitle := func(boards []*Board, title string) *Board {
for _, b := range boards {
if b.Title == title {
return b
}
}
return nil
}
t.Run("invalid boards and blocks", func(t *testing.T) {
bab := &BoardsAndBlocks{
Blocks: []*Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard},
},
}
rBab, err := GenerateBoardsAndBlocksIDs(bab, logger)
require.Error(t, err)
require.Nil(t, rBab)
})
t.Run("correctly generates IDs for all the boards and links the blocks to them, with new IDs too", func(t *testing.T) {
bab := &BoardsAndBlocks{
Boards: []*Board{
{ID: "board-id-1", Type: BoardTypeOpen, Title: "board1"},
{ID: "board-id-2", Type: BoardTypePrivate, Title: "board2"},
{ID: "board-id-3", Type: BoardTypeOpen, Title: "board3"},
},
Blocks: []*Block{
{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard},
{ID: "block-id-2", BoardID: "board-id-2", Type: TypeView},
{ID: "block-id-3", BoardID: "board-id-2", Type: TypeText},
},
}
rBab, err := GenerateBoardsAndBlocksIDs(bab, logger)
require.NoError(t, err)
require.NotNil(t, rBab)
// all boards and blocks should have refreshed their IDs, and
// blocks should be correctly linked to the new board IDs
board1 := getBoardByTitle(rBab.Boards, "board1")
require.NotNil(t, board1)
require.NotEmpty(t, board1.ID)
require.NotEqual(t, "board-id-1", board1.ID)
board2 := getBoardByTitle(rBab.Boards, "board2")
require.NotNil(t, board2)
require.NotEmpty(t, board2.ID)
require.NotEqual(t, "board-id-2", board2.ID)
board3 := getBoardByTitle(rBab.Boards, "board3")
require.NotNil(t, board3)
require.NotEmpty(t, board3.ID)
require.NotEqual(t, "board-id-3", board3.ID)
block1 := getBlockByType(rBab.Blocks, TypeCard)
require.NotNil(t, block1)
require.NotEmpty(t, block1.ID)
require.NotEqual(t, "block-id-1", block1.ID)
require.Equal(t, board1.ID, block1.BoardID)
block2 := getBlockByType(rBab.Blocks, TypeView)
require.NotNil(t, block2)
require.NotEmpty(t, block2.ID)
require.NotEqual(t, "block-id-2", block2.ID)
require.Equal(t, board2.ID, block2.BoardID)
block3 := getBlockByType(rBab.Blocks, TypeText)
require.NotNil(t, block3)
require.NotEmpty(t, block3.ID)
require.NotEqual(t, "block-id-3", block3.ID)
require.Equal(t, board2.ID, block3.BoardID)
})
}
func TestIsValidPatchBoardsAndBlocks(t *testing.T) {
newTitle := "new title"
newDescription := "new description"
var schema int64 = 1
t.Run("no board ids", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
{Schema: &schema},
},
}
require.ErrorIs(t, pbab.IsValid(), ErrNoBoardsInBoardsAndBlocks)
})
t.Run("missmatch board IDs and patches", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{"board-id-1", "board-id-2"},
BoardPatches: []*BoardPatch{
{Title: &newTitle},
},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
},
}
require.ErrorIs(t, pbab.IsValid(), ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks)
})
t.Run("missmatch block IDs and patches", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{"board-id-1", "board-id-2"},
BoardPatches: []*BoardPatch{
{Title: &newTitle},
{Description: &newDescription},
},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
{Schema: &schema},
},
}
require.ErrorIs(t, pbab.IsValid(), ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks)
})
t.Run("valid", func(t *testing.T) {
pbab := &PatchBoardsAndBlocks{
BoardIDs: []string{"board-id-1", "board-id-2"},
BoardPatches: []*BoardPatch{
{Title: &newTitle},
{Description: &newDescription},
},
BlockIDs: []string{"block-id-1"},
BlockPatches: []*BlockPatch{
{Title: &newTitle},
},
}
require.NoError(t, pbab.IsValid())
})
}
func TestIsValidDeleteBoardsAndBlocks(t *testing.T) {
/*
TODO fix this
t.Run("no board ids", func(t *testing.T) {
dbab := &DeleteBoardsAndBlocks{
TeamID: "team-id",
Blocks: []string{"block-id-1"},
}
require.ErrorIs(t, dbab.IsValid(), NoBoardsInBoardsAndBlocksErr)
})
t.Run("no block ids", func(t *testing.T) {
dbab := &DeleteBoardsAndBlocks{
TeamID: "team-id",
Boards: []string{"board-id-1", "board-id-2"},
}
require.ErrorIs(t, dbab.IsValid(), NoBlocksInBoardsAndBlocksErr)
})
t.Run("valid", func(t *testing.T) {
dbab := &DeleteBoardsAndBlocks{
TeamID: "team-id",
Boards: []string{"board-id-1", "board-id-2"},
Blocks: []string{"block-id-1"},
}
require.NoError(t, dbab.IsValid())
})
*/
}

327
server/boards/model/card.go Обычный файл
Просмотреть файл

@@ -0,0 +1,327 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"errors"
"fmt"
"github.com/rivo/uniseg"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
var ErrBoardIDMismatch = errors.New("Board IDs do not match")
type ErrInvalidCard struct {
msg string
}
func NewErrInvalidCard(msg string) ErrInvalidCard {
return ErrInvalidCard{
msg: msg,
}
}
func (e ErrInvalidCard) Error() string {
return fmt.Sprintf("invalid card, %s", e.msg)
}
var ErrNotCardBlock = errors.New("not a card block")
type ErrInvalidFieldType struct {
field string
}
func (e ErrInvalidFieldType) Error() string {
return fmt.Sprintf("invalid type for field '%s'", e.field)
}
// Card represents a group of content blocks and properties.
// swagger:model
type Card struct {
// The id for this card
// required: false
ID string `json:"id"`
// The id for board this card belongs to.
// required: false
BoardID string `json:"boardId"`
// The id for user who created this card
// required: false
CreatedBy string `json:"createdBy"`
// The id for user who last modified this card
// required: false
ModifiedBy string `json:"modifiedBy"`
// The display title
// required: false
Title string `json:"title"`
// An array of content block ids specifying the ordering of content for this card.
// required: false
ContentOrder []string `json:"contentOrder"`
// The icon of the card
// required: false
Icon string `json:"icon"`
// True if this card belongs to a template
// required: false
IsTemplate bool `json:"isTemplate"`
// A map of property ids to property values (option ids, strings, array of option ids)
// required: false
Properties map[string]any `json:"properties"`
// The creation time in milliseconds since the current epoch
// required: false
CreateAt int64 `json:"createAt"`
// The last modified time in milliseconds since the current epoch
// required: false
UpdateAt int64 `json:"updateAt"`
// The deleted time in milliseconds since the current epoch. Set to indicate this card is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
}
// Populate populates a Card with default values.
func (c *Card) Populate() {
if c.ID == "" {
c.ID = utils.NewID(utils.IDTypeCard)
}
if c.ContentOrder == nil {
c.ContentOrder = make([]string, 0)
}
if c.Properties == nil {
c.Properties = make(map[string]any)
}
now := utils.GetMillis()
if c.CreateAt == 0 {
c.CreateAt = now
}
if c.UpdateAt == 0 {
c.UpdateAt = now
}
}
func (c *Card) PopulateWithBoardID(boardID string) {
c.BoardID = boardID
c.Populate()
}
// CheckValid returns an error if the Card has invalid field values.
func (c *Card) CheckValid() error {
if c.ID == "" {
return ErrInvalidCard{"ID is missing"}
}
if c.BoardID == "" {
return ErrInvalidCard{"BoardID is missing"}
}
if c.ContentOrder == nil {
return ErrInvalidCard{"ContentOrder is missing"}
}
if uniseg.GraphemeClusterCount(c.Icon) > 1 {
return ErrInvalidCard{"Icon can have only one grapheme"}
}
if c.Properties == nil {
return ErrInvalidCard{"Properties"}
}
if c.CreateAt == 0 {
return ErrInvalidCard{"CreateAt"}
}
if c.UpdateAt == 0 {
return ErrInvalidCard{"UpdateAt"}
}
return nil
}
// CardPatch is a patch for modifying cards
// swagger:model
type CardPatch struct {
// The display title
// required: false
Title *string `json:"title"`
// An array of content block ids specifying the ordering of content for this card.
// required: false
ContentOrder *[]string `json:"contentOrder"`
// The icon of the card
// required: false
Icon *string `json:"icon"`
// A map of property ids to property option ids to be updated
// required: false
UpdatedProperties map[string]any `json:"updatedProperties"`
}
// Patch returns an updated version of the card.
func (p *CardPatch) Patch(card *Card) *Card {
if p.Title != nil {
card.Title = *p.Title
}
if p.ContentOrder != nil {
card.ContentOrder = *p.ContentOrder
}
if p.Icon != nil {
card.Icon = *p.Icon
}
if card.Properties == nil {
card.Properties = make(map[string]any)
}
// if there are properties marked for update, we replace the
// existing ones or add them
for propID, propVal := range p.UpdatedProperties {
card.Properties[propID] = propVal
}
return card
}
// CheckValid returns an error if the CardPatch has invalid field values.
func (p *CardPatch) CheckValid() error {
if p.Icon != nil && uniseg.GraphemeClusterCount(*p.Icon) > 1 {
return ErrInvalidCard{"Icon can have only one grapheme"}
}
return nil
}
// Card2Block converts a card to block using a shallow copy. Not needed once cards are first class entities.
func Card2Block(card *Card) *Block {
fields := make(map[string]interface{})
fields["contentOrder"] = card.ContentOrder
fields["icon"] = card.Icon
fields["isTemplate"] = card.IsTemplate
fields["properties"] = card.Properties
return &Block{
ID: card.ID,
ParentID: card.BoardID,
CreatedBy: card.CreatedBy,
ModifiedBy: card.ModifiedBy,
Schema: 1,
Type: TypeCard,
Title: card.Title,
Fields: fields,
CreateAt: card.CreateAt,
UpdateAt: card.UpdateAt,
DeleteAt: card.DeleteAt,
BoardID: card.BoardID,
}
}
// Block2Card converts a block to a card. Not needed once cards are first class entities.
func Block2Card(block *Block) (*Card, error) {
if block.Type != TypeCard {
return nil, fmt.Errorf("cannot convert block to card: %w", ErrNotCardBlock)
}
contentOrder := make([]string, 0)
icon := ""
isTemplate := false
properties := make(map[string]any)
if co, ok := block.Fields["contentOrder"]; ok {
switch arr := co.(type) {
case []any:
for _, str := range arr {
if id, ok := str.(string); ok {
contentOrder = append(contentOrder, id)
} else {
return nil, ErrInvalidFieldType{"contentOrder item"}
}
}
case []string:
contentOrder = append(contentOrder, arr...)
default:
return nil, ErrInvalidFieldType{"contentOrder"}
}
}
if iconAny, ok := block.Fields["icon"]; ok {
if id, ok := iconAny.(string); ok {
icon = id
} else {
return nil, ErrInvalidFieldType{"icon"}
}
}
if isTemplateAny, ok := block.Fields["isTemplate"]; ok {
if b, ok := isTemplateAny.(bool); ok {
isTemplate = b
} else {
return nil, ErrInvalidFieldType{"isTemplate"}
}
}
if props, ok := block.Fields["properties"]; ok {
if propMap, ok := props.(map[string]any); ok {
for k, v := range propMap {
properties[k] = v
}
} else {
return nil, ErrInvalidFieldType{"properties"}
}
}
card := &Card{
ID: block.ID,
BoardID: block.BoardID,
CreatedBy: block.CreatedBy,
ModifiedBy: block.ModifiedBy,
Title: block.Title,
ContentOrder: contentOrder,
Icon: icon,
IsTemplate: isTemplate,
Properties: properties,
CreateAt: block.CreateAt,
UpdateAt: block.UpdateAt,
DeleteAt: block.DeleteAt,
}
card.Populate()
return card, nil
}
// CardPatch2BlockPatch converts a CardPatch to a BlockPatch. Not needed once cards are first class entities.
func CardPatch2BlockPatch(cardPatch *CardPatch) (*BlockPatch, error) {
if err := cardPatch.CheckValid(); err != nil {
return nil, err
}
blockPatch := &BlockPatch{
Title: cardPatch.Title,
}
updatedFields := make(map[string]any, 0)
if cardPatch.ContentOrder != nil {
updatedFields["contentOrder"] = cardPatch.ContentOrder
}
if cardPatch.Icon != nil {
updatedFields["icon"] = cardPatch.Icon
}
properties := make(map[string]any)
for k, v := range cardPatch.UpdatedProperties {
properties[k] = v
}
if len(properties) != 0 {
updatedFields["properties"] = cardPatch.UpdatedProperties
}
blockPatch.UpdatedFields = updatedFields
return blockPatch, nil
}

88
server/boards/model/card_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,88 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
func TestBlock2Card(t *testing.T) {
blockID := utils.NewID(utils.IDTypeCard)
boardID := utils.NewID(utils.IDTypeBoard)
userID := utils.NewID(utils.IDTypeUser)
now := utils.GetMillis()
var fields map[string]any
err := json.Unmarshal([]byte(sampleBlockFieldsJSON), &fields)
require.NoError(t, err)
block := &Block{
ID: blockID,
ParentID: boardID,
CreatedBy: userID,
ModifiedBy: userID,
Schema: 1,
Type: TypeCard,
Title: "My card title",
Fields: fields,
CreateAt: now,
UpdateAt: now,
DeleteAt: 0,
BoardID: boardID,
}
t.Run("Good block", func(t *testing.T) {
card, err := Block2Card(block)
require.NoError(t, err)
assert.Equal(t, block.ID, card.ID)
assert.Equal(t, []string{"acdxa8r8aht85pyoeuj1ed7tu8w", "73urm1huoupd4idzkdq5yaeuyay", "ay6sogs9owtd9xbyn49qt3395ko"}, card.ContentOrder)
assert.EqualValues(t, fields["icon"], card.Icon)
assert.EqualValues(t, fields["isTemplate"], card.IsTemplate)
assert.EqualValues(t, fields["properties"], card.Properties)
})
t.Run("Not a card", func(t *testing.T) {
blockNotCard := &Block{}
card, err := Block2Card(blockNotCard)
require.Error(t, err)
require.Nil(t, card)
})
}
const sampleBlockFieldsJSON = `
{
"contentOrder":[
"acdxa8r8aht85pyoeuj1ed7tu8w",
"73urm1huoupd4idzkdq5yaeuyay",
"ay6sogs9owtd9xbyn49qt3395ko"
],
"icon":"🎨",
"isTemplate":false,
"properties":{
"aa7swu9zz3ofdkcna3h867cum4y":"212-444-1234",
"af6fcbb8-ca56-4b73-83eb-37437b9a667d":"77c539af-309c-4db1-8329-d20ef7e9eacd",
"aiwt9ibi8jjrf9hzi1xzk8no8mo":"foo",
"aj65h4s6ghr6wgh3bnhqbzzmiaa":"77",
"ajy6xbebzopojaenbnmfpgtdwso":"{\"from\":1660046400000}",
"amc8wnk1xqj54rymkoqffhtw7ie":"zhqsoeqs1pg9i8gk81k9ryy83h",
"aooz77t119y7xtfmoyeiy4up75c":"someone@example.com",
"auskzaoaccsn55icuwarf4o3tfe":"https://www.google.com",
"aydsk41h6cs1z7nmghaw16jqcia":[
"aw565znut6zphbxqhbwyawiuggy",
"aefd3pxciomrkur4rc6smg1usoc",
"a6c96kwrqaskbtochq9wunmzweh",
"atyexeuq993fwwb84bxoqixxqqr"
],
"d6b1249b-bc18-45fc-889e-bec48fce80ef":"9a090e33-b110-4268-8909-132c5002c90e",
"d9725d14-d5a8-48e5-8de1-6f8c004a9680":"3245a32d-f688-463b-87f4-8e7142c1b397"
}
}`

118
server/boards/model/category.go Обычный файл
Просмотреть файл

@@ -0,0 +1,118 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
const (
CategoryTypeSystem = "system"
CategoryTypeCustom = "custom"
)
// Category is a board category
// swagger:model
type Category struct {
// The id for this category
// required: true
ID string `json:"id"`
// The name for this category
// required: true
Name string `json:"name"`
// The user's id for this category
// required: true
UserID string `json:"userID"`
// The team id for this category
// required: true
TeamID string `json:"teamID"`
// The creation time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// The last modified time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
// The deleted time in miliseconds since the current epoch. Set to indicate this category is deleted
// required: false
DeleteAt int64 `json:"deleteAt"`
// Category's state in client side
// required: true
Collapsed bool `json:"collapsed"`
// Inter-category sort order per user
// required: true
SortOrder int `json:"sortOrder"`
// The sorting method applied on this category
// required: true
Sorting string `json:"sorting"`
// Category's type
// required: true
Type string `json:"type"`
}
func (c *Category) Hydrate() {
if c.ID == "" {
c.ID = utils.NewID(utils.IDTypeNone)
}
if c.CreateAt == 0 {
c.CreateAt = utils.GetMillis()
}
if c.UpdateAt == 0 {
c.UpdateAt = c.CreateAt
}
if c.SortOrder < 0 {
c.SortOrder = 0
}
if strings.TrimSpace(c.Type) == "" {
c.Type = CategoryTypeCustom
}
}
func (c *Category) IsValid() error {
if strings.TrimSpace(c.ID) == "" {
return NewErrInvalidCategory("category ID cannot be empty")
}
if strings.TrimSpace(c.Name) == "" {
return NewErrInvalidCategory("category name cannot be empty")
}
if strings.TrimSpace(c.UserID) == "" {
return NewErrInvalidCategory("category user ID cannot be empty")
}
if strings.TrimSpace(c.TeamID) == "" {
return NewErrInvalidCategory("category team id ID cannot be empty")
}
if c.Type != CategoryTypeCustom && c.Type != CategoryTypeSystem {
return NewErrInvalidCategory(fmt.Sprintf("category type is invalid. Allowed types: %s and %s", CategoryTypeSystem, CategoryTypeCustom))
}
return nil
}
func CategoryFromJSON(data io.Reader) *Category {
var category *Category
_ = json.NewDecoder(data).Decode(&category)
return category
}

31
server/boards/model/category_boards.go Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const CategoryBoardsSortOrderGap = 10
// CategoryBoards is a board category and associated boards
// swagger:model
type CategoryBoards struct {
Category
// The IDs of boards in this category
// required: true
BoardMetadata []CategoryBoardMetadata `json:"boardMetadata"`
// The relative sort order of this board in its category
// required: true
SortOrder int `json:"sortOrder"`
}
type BoardCategoryWebsocketData struct {
BoardID string `json:"boardID"`
CategoryID string `json:"categoryID"`
Hidden bool `json:"hidden"`
}
type CategoryBoardMetadata struct {
BoardID string `json:"boardID"`
Hidden bool `json:"hidden"`
}

32
server/boards/model/clientConfig.go Обычный файл
Просмотреть файл

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// ClientConfig is the client configuration
// swagger:model
type ClientConfig struct {
// Is telemetry enabled
// required: true
Telemetry bool `json:"telemetry"`
// The telemetry ID
// required: true
TelemetryID string `json:"telemetryid"`
// Is public shared boards enabled
// required: true
EnablePublicSharedBoards bool `json:"enablePublicSharedBoards"`
// Is public shared boards enabled
// required: true
TeammateNameDisplay string `json:"teammateNameDisplay"`
// The server feature flags
// required: true
FeatureFlags map[string]string `json:"featureFlags"`
// Required for file upload to check the size of the file
// required: true
MaxFileSize int64 `json:"maxFileSize"`
}

27
server/boards/model/cloud.go Обычный файл
Просмотреть файл

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const LimitUnlimited = 0
// BoardsCloudLimits is the representation of the limits for the
// Boards server
// swagger:model
type BoardsCloudLimits struct {
// The maximum number of cards on the server
// required: true
Cards int `json:"cards"`
// The current number of cards on the server
// required: true
UsedCards int `json:"used_cards"`
// The updated_at timestamp of the limit card
// required: true
CardLimitTimestamp int64 `json:"card_limit_timestamp"`
// The maximum number of views for each board
// required: true
Views int `json:"views"`
}

88
server/boards/model/compliance.go Обычный файл
Просмотреть файл

@@ -0,0 +1,88 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// BaordsComplianceResponse is the response body to a request for boards.
// swagger:model
type BoardsComplianceResponse struct {
// True if there is a next page for pagination
// required: true
HasNext bool `json:"hasNext"`
// The array of board records.
// required: true
Results []*Board `json:"results"`
}
// BoardsComplianceHistoryResponse is the response body to a request for boards history.
// swagger:model
type BoardsComplianceHistoryResponse struct {
// True if there is a next page for pagination
// required: true
HasNext bool `json:"hasNext"`
// The array of BoardHistory records.
// required: true
Results []*BoardHistory `json:"results"`
}
// BlocksComplianceHistoryResponse is the response body to a request for blocks history.
// swagger:model
type BlocksComplianceHistoryResponse struct {
// True if there is a next page for pagination
// required: true
HasNext bool `json:"hasNext"`
// The array of BlockHistory records.
// required: true
Results []*BlockHistory `json:"results"`
}
// BoardHistory provides information about the history of a board.
// swagger:model
type BoardHistory struct {
ID string `json:"id"`
TeamID string `json:"teamId"`
IsDeleted bool `json:"isDeleted"`
DescendantLastUpdateAt int64 `json:"descendantLastUpdateAt"`
DescendantFirstUpdateAt int64 `json:"descendantFirstUpdateAt"`
CreatedBy string `json:"createdBy"`
LastModifiedBy string `json:"lastModifiedBy"`
}
// BlockHistory provides information about the history of a block.
// swagger:model
type BlockHistory struct {
ID string `json:"id"`
TeamID string `json:"teamId"`
BoardID string `json:"boardId"`
Type string `json:"type"`
IsDeleted bool `json:"isDeleted"`
LastUpdateAt int64 `json:"lastUpdateAt"`
FirstUpdateAt int64 `json:"firstUpdateAt"`
CreatedBy string `json:"createdBy"`
LastModifiedBy string `json:"lastModifiedBy"`
}
type QueryBoardsForComplianceOptions struct {
TeamID string // if not empty then filter for specific team, otherwise all teams are included
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=60)
}
type QueryBoardsComplianceHistoryOptions struct {
ModifiedSince int64 // if non-zero then filter for records with update_at greater than ModifiedSince
IncludeDeleted bool // if true then deleted blocks are included
TeamID string // if not empty then filter for specific team, otherwise all teams are included
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=60)
}
type QueryBlocksComplianceHistoryOptions struct {
ModifiedSince int64 // if non-zero then filter for records with update_at greater than ModifiedSince
IncludeDeleted bool // if true then deleted blocks are included
TeamID string // if not empty then filter for specific team, otherwise all teams are included
BoardID string // if not empty then filter for specific board, otherwise all boards are included
Page int // page number to select when paginating
PerPage int // number of blocks per page (default=60)
}

9
server/boards/model/database.go Обычный файл
Просмотреть файл

@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
const (
PostgresDBType = "postgres"
MysqlDBType = "mysql"
)

314
server/boards/model/error.go Обычный файл
Просмотреть файл

@@ -0,0 +1,314 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"database/sql"
"errors"
"fmt"
"net/http"
"strings"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
var (
ErrViewsLimitReached = errors.New("views limit reached for board")
ErrPatchUpdatesLimitedCards = errors.New("patch updates cards that are limited")
ErrInsufficientLicense = errors.New("appropriate license required")
ErrCategoryPermissionDenied = errors.New("category doesn't belong to user")
ErrCategoryDeleted = errors.New("category is deleted")
ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins")
ErrRequestEntityTooLarge = errors.New("request entity too large")
ErrInvalidBoardSearchField = errors.New("invalid board search field")
)
// ErrNotFound is an error type that can be returned by store APIs
// when a query unexpectedly fetches no records.
type ErrNotFound struct {
entity string
}
// NewErrNotFound creates a new ErrNotFound instance.
func NewErrNotFound(entity string) *ErrNotFound {
return &ErrNotFound{
entity: entity,
}
}
func (nf *ErrNotFound) Error() string {
return fmt.Sprintf("{%s} not found", nf.entity)
}
// ErrNotAllFound is an error type that can be returned by store APIs
// when a query that should fetch a certain amount of records
// unexpectedly fetches less.
type ErrNotAllFound struct {
entity string
resources []string
}
func NewErrNotAllFound(entity string, resources []string) *ErrNotAllFound {
return &ErrNotAllFound{
entity: entity,
resources: resources,
}
}
func (naf *ErrNotAllFound) Error() string {
return fmt.Sprintf("not all instances of {%s} in {%s} found", naf.entity, strings.Join(naf.resources, ", "))
}
// ErrBadRequest can be returned when the API handler receives a
// malformed request.
type ErrBadRequest struct {
reason string
}
// NewErrNotFound creates a new ErrNotFound instance.
func NewErrBadRequest(reason string) *ErrBadRequest {
return &ErrBadRequest{
reason: reason,
}
}
func (br *ErrBadRequest) Error() string {
return br.reason
}
// ErrUnauthorized can be returned when requester has provided an
// invalid authorization for a given resource or has not provided any.
type ErrUnauthorized struct {
reason string
}
// NewErrUnauthorized creates a new ErrUnauthorized instance.
func NewErrUnauthorized(reason string) *ErrUnauthorized {
return &ErrUnauthorized{
reason: reason,
}
}
func (br *ErrUnauthorized) Error() string {
return br.reason
}
// ErrPermission can be returned when requester lacks a permission for
// a given resource.
type ErrPermission struct {
reason string
}
// NewErrPermission creates a new ErrPermission instance.
func NewErrPermission(reason string) *ErrPermission {
return &ErrPermission{
reason: reason,
}
}
func (br *ErrPermission) Error() string {
return br.reason
}
// ErrForbidden can be returned when requester doesn't have access to
// a given resource.
type ErrForbidden struct {
reason string
}
// NewErrForbidden creates a new ErrForbidden instance.
func NewErrForbidden(reason string) *ErrForbidden {
return &ErrForbidden{
reason: reason,
}
}
func (br *ErrForbidden) Error() string {
return br.reason
}
type ErrInvalidCategory struct {
msg string
}
func NewErrInvalidCategory(msg string) *ErrInvalidCategory {
return &ErrInvalidCategory{
msg: msg,
}
}
func (e *ErrInvalidCategory) Error() string {
return e.msg
}
type ErrNotImplemented struct {
msg string
}
func NewErrNotImplemented(msg string) *ErrNotImplemented {
return &ErrNotImplemented{
msg: msg,
}
}
func (ni *ErrNotImplemented) Error() string {
return ni.msg
}
// IsErrBadRequest returns true if `err` is or wraps one of:
// - model.ErrBadRequest
// - model.ErrViewsLimitReached
// - model.ErrAuthParam
// - model.ErrInvalidCategory
// - model.ErrBoardMemberIsLastAdmin
// - model.ErrBoardIDMismatch.
func IsErrBadRequest(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrBadRequest
var br *ErrBadRequest
if errors.As(err, &br) {
return true
}
// check if this is a model.ErrAuthParam
var ap *ErrAuthParam
if errors.As(err, &ap) {
return true
}
// check if this is a model.ErrViewsLimitReached
if errors.Is(err, ErrViewsLimitReached) {
return true
}
// check if this is a model.ErrInvalidCategory
var ic *ErrInvalidCategory
if errors.As(err, &ic) {
return true
}
// check if this is a model.ErrBoardIDMismatch
if errors.Is(err, ErrBoardMemberIsLastAdmin) {
return true
}
// check if this is a model.ErrBoardMemberIsLastAdmin
return errors.Is(err, ErrBoardIDMismatch)
}
// IsErrUnauthorized returns true if `err` is or wraps one of:
// - model.ErrUnauthorized.
func IsErrUnauthorized(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrUnauthorized
var u *ErrUnauthorized
return errors.As(err, &u)
}
// IsErrForbidden returns true if `err` is or wraps one of:
// - model.ErrForbidden
// - model.ErrPermission
// - model.ErrPatchUpdatesLimitedCards
// - model.ErrorCategoryPermissionDenied.
func IsErrForbidden(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrForbidden
var f *ErrForbidden
if errors.As(err, &f) {
return true
}
// check if this is a model.ErrPermission
var p *ErrPermission
if errors.As(err, &p) {
return true
}
// check if this is a model.ErrPatchUpdatesLimitedCards
if errors.Is(err, ErrPatchUpdatesLimitedCards) {
return true
}
// check if this is a model.ErrCategoryPermissionDenied
return errors.Is(err, ErrCategoryPermissionDenied)
}
// IsErrNotFound returns true if `err` is or wraps one of:
// - model.ErrNotFound
// - model.ErrNotAllFound
// - sql.ErrNoRows
// - mattermost-plugin-api/ErrNotFound.
// - model.ErrCategoryDeleted.
func IsErrNotFound(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrNotFound
var nf *ErrNotFound
if errors.As(err, &nf) {
return true
}
// check if this is a model.ErrNotAllFound
var naf *ErrNotAllFound
if errors.As(err, &naf) {
return true
}
// check if this is a sql.ErrNotFound
if errors.Is(err, sql.ErrNoRows) {
return true
}
// check if this is a Mattermost AppError with a Not Found status
var appErr *mm_model.AppError
if errors.As(err, &appErr) {
if appErr.StatusCode == http.StatusNotFound {
return true
}
}
// check if this is a model.ErrCategoryDeleted
return errors.Is(err, ErrCategoryDeleted)
}
// IsErrRequestEntityTooLarge returns true if `err` is or wraps one of:
// - model.ErrRequestEntityTooLarge.
func IsErrRequestEntityTooLarge(err error) bool {
// check if this is a model.ErrRequestEntityTooLarge
return errors.Is(err, ErrRequestEntityTooLarge)
}
// IsErrNotImplemented returns true if `err` is or wraps one of:
// - model.ErrNotImplemented
// - model.ErrInsufficientLicense.
func IsErrNotImplemented(err error) bool {
if err == nil {
return false
}
// check if this is a model.ErrNotImplemented
var eni *ErrNotImplemented
if errors.As(err, &eni) {
return true
}
// check if this is a model.ErrInsufficientLicense
return errors.Is(err, ErrInsufficientLicense)
}

16
server/boards/model/errorResponse.go Обычный файл
Просмотреть файл

@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
// ErrorResponse is an error response
// swagger:model
type ErrorResponse struct {
// The error message
// required: false
Error string `json:"error"`
// The error code
// required: false
ErrorCode int `json:"errorCode"`
}

90
server/boards/model/import_export.go Обычный файл
Просмотреть файл

@@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"errors"
"fmt"
)
var (
ErrInvalidImageBlock = errors.New("invalid image block")
)
// Archive is an import / export archive.
// TODO: remove once default templates are converted to new archive format.
type Archive struct {
Version int64 `json:"version"`
Date int64 `json:"date"`
Blocks []Block `json:"blocks"`
}
// ArchiveHeader is the content of the first file (`version.json`) within an archive.
type ArchiveHeader struct {
Version int `json:"version"`
Date int64 `json:"date"`
}
// ArchiveLine is any line in an archive.
type ArchiveLine struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
// ExportArchiveOptions provides options when exporting one or more boards
// to an archive.
type ExportArchiveOptions struct {
TeamID string
// BoardIDs is the list of boards to include in the archive.
// Empty slice means export all boards from workspace/team.
BoardIDs []string
}
// ImportArchiveOptions provides options when importing an archive.
type ImportArchiveOptions struct {
TeamID string
ModifiedBy string
BoardModifier BoardModifier
BlockModifier BlockModifier
}
// ErrUnsupportedArchiveVersion is an error returned when trying to import an
// archive with a version that this server does not support.
type ErrUnsupportedArchiveVersion struct {
got int
want int
}
// NewErrUnsupportedArchiveVersion creates a ErrUnsupportedArchiveVersion error.
func NewErrUnsupportedArchiveVersion(got int, want int) ErrUnsupportedArchiveVersion {
return ErrUnsupportedArchiveVersion{
got: got,
want: want,
}
}
func (e ErrUnsupportedArchiveVersion) Error() string {
return fmt.Sprintf("unsupported archive version; got %d, want %d", e.got, e.want)
}
// ErrUnsupportedArchiveLineType is an error returned when trying to import an
// archive containing an unsupported line type.
type ErrUnsupportedArchiveLineType struct {
line int
got string
}
// NewErrUnsupportedArchiveLineType creates a ErrUnsupportedArchiveLineType error.
func NewErrUnsupportedArchiveLineType(line int, got string) ErrUnsupportedArchiveLineType {
return ErrUnsupportedArchiveLineType{
line: line,
got: got,
}
}
func (e ErrUnsupportedArchiveLineType) Error() string {
return fmt.Sprintf("unsupported archive line type; got %s, line %d", e.got, e.line)
}

490
server/boards/model/mocks/mockservicesapi.go Обычный файл
Просмотреть файл

@@ -0,0 +1,490 @@
// 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/model (interfaces: ServicesAPI)
// Package mocks is a generated GoMock package.
package mocks
import (
sql "database/sql"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
mux "github.com/gorilla/mux"
model "github.com/mattermost/mattermost-server/v6/model"
mlog "github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// MockServicesAPI is a mock of ServicesAPI interface.
type MockServicesAPI struct {
ctrl *gomock.Controller
recorder *MockServicesAPIMockRecorder
}
// MockServicesAPIMockRecorder is the mock recorder for MockServicesAPI.
type MockServicesAPIMockRecorder struct {
mock *MockServicesAPI
}
// NewMockServicesAPI creates a new mock instance.
func NewMockServicesAPI(ctrl *gomock.Controller) *MockServicesAPI {
mock := &MockServicesAPI{ctrl: ctrl}
mock.recorder = &MockServicesAPIMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockServicesAPI) EXPECT() *MockServicesAPIMockRecorder {
return m.recorder
}
// CreateMember mocks base method.
func (m *MockServicesAPI) CreateMember(arg0, arg1 string) (*model.TeamMember, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateMember", arg0, arg1)
ret0, _ := ret[0].(*model.TeamMember)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateMember indicates an expected call of CreateMember.
func (mr *MockServicesAPIMockRecorder) CreateMember(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMember", reflect.TypeOf((*MockServicesAPI)(nil).CreateMember), arg0, arg1)
}
// CreatePost mocks base method.
func (m *MockServicesAPI) CreatePost(arg0 *model.Post) (*model.Post, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreatePost", arg0)
ret0, _ := ret[0].(*model.Post)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreatePost indicates an expected call of CreatePost.
func (mr *MockServicesAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockServicesAPI)(nil).CreatePost), arg0)
}
// DeletePreferencesForUser mocks base method.
func (m *MockServicesAPI) DeletePreferencesForUser(arg0 string, arg1 model.Preferences) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeletePreferencesForUser", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// DeletePreferencesForUser indicates an expected call of DeletePreferencesForUser.
func (mr *MockServicesAPIMockRecorder) DeletePreferencesForUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).DeletePreferencesForUser), arg0, arg1)
}
// EnsureBot mocks base method.
func (m *MockServicesAPI) EnsureBot(arg0 *model.Bot) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsureBot", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EnsureBot indicates an expected call of EnsureBot.
func (mr *MockServicesAPIMockRecorder) EnsureBot(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBot", reflect.TypeOf((*MockServicesAPI)(nil).EnsureBot), arg0)
}
// GetChannelByID mocks base method.
func (m *MockServicesAPI) GetChannelByID(arg0 string) (*model.Channel, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChannelByID", arg0)
ret0, _ := ret[0].(*model.Channel)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChannelByID indicates an expected call of GetChannelByID.
func (mr *MockServicesAPIMockRecorder) GetChannelByID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelByID", reflect.TypeOf((*MockServicesAPI)(nil).GetChannelByID), arg0)
}
// GetChannelMember mocks base method.
func (m *MockServicesAPI) GetChannelMember(arg0, arg1 string) (*model.ChannelMember, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChannelMember", arg0, arg1)
ret0, _ := ret[0].(*model.ChannelMember)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChannelMember indicates an expected call of GetChannelMember.
func (mr *MockServicesAPIMockRecorder) GetChannelMember(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelMember", reflect.TypeOf((*MockServicesAPI)(nil).GetChannelMember), arg0, arg1)
}
// GetChannelsForTeamForUser mocks base method.
func (m *MockServicesAPI) GetChannelsForTeamForUser(arg0, arg1 string, arg2 bool) (model.ChannelList, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetChannelsForTeamForUser", arg0, arg1, arg2)
ret0, _ := ret[0].(model.ChannelList)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetChannelsForTeamForUser indicates an expected call of GetChannelsForTeamForUser.
func (mr *MockServicesAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelsForTeamForUser", reflect.TypeOf((*MockServicesAPI)(nil).GetChannelsForTeamForUser), arg0, arg1, arg2)
}
// GetCloudLimits mocks base method.
func (m *MockServicesAPI) GetCloudLimits() (*model.ProductLimits, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCloudLimits")
ret0, _ := ret[0].(*model.ProductLimits)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCloudLimits indicates an expected call of GetCloudLimits.
func (mr *MockServicesAPIMockRecorder) GetCloudLimits() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockServicesAPI)(nil).GetCloudLimits))
}
// GetConfig mocks base method.
func (m *MockServicesAPI) GetConfig() *model.Config {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetConfig")
ret0, _ := ret[0].(*model.Config)
return ret0
}
// GetConfig indicates an expected call of GetConfig.
func (mr *MockServicesAPIMockRecorder) GetConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockServicesAPI)(nil).GetConfig))
}
// GetDiagnosticID mocks base method.
func (m *MockServicesAPI) GetDiagnosticID() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDiagnosticID")
ret0, _ := ret[0].(string)
return ret0
}
// GetDiagnosticID indicates an expected call of GetDiagnosticID.
func (mr *MockServicesAPIMockRecorder) GetDiagnosticID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDiagnosticID", reflect.TypeOf((*MockServicesAPI)(nil).GetDiagnosticID))
}
// GetDirectChannel mocks base method.
func (m *MockServicesAPI) GetDirectChannel(arg0, arg1 string) (*model.Channel, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDirectChannel", arg0, arg1)
ret0, _ := ret[0].(*model.Channel)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDirectChannel indicates an expected call of GetDirectChannel.
func (mr *MockServicesAPIMockRecorder) GetDirectChannel(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannel", reflect.TypeOf((*MockServicesAPI)(nil).GetDirectChannel), arg0, arg1)
}
// GetDirectChannelOrCreate mocks base method.
func (m *MockServicesAPI) GetDirectChannelOrCreate(arg0, arg1 string) (*model.Channel, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDirectChannelOrCreate", arg0, arg1)
ret0, _ := ret[0].(*model.Channel)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDirectChannelOrCreate indicates an expected call of GetDirectChannelOrCreate.
func (mr *MockServicesAPIMockRecorder) GetDirectChannelOrCreate(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDirectChannelOrCreate", reflect.TypeOf((*MockServicesAPI)(nil).GetDirectChannelOrCreate), arg0, arg1)
}
// GetFileInfo mocks base method.
func (m *MockServicesAPI) GetFileInfo(arg0 string) (*model.FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFileInfo", arg0)
ret0, _ := ret[0].(*model.FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFileInfo indicates an expected call of GetFileInfo.
func (mr *MockServicesAPIMockRecorder) GetFileInfo(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileInfo", reflect.TypeOf((*MockServicesAPI)(nil).GetFileInfo), arg0)
}
// GetLicense mocks base method.
func (m *MockServicesAPI) GetLicense() *model.License {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLicense")
ret0, _ := ret[0].(*model.License)
return ret0
}
// GetLicense indicates an expected call of GetLicense.
func (mr *MockServicesAPIMockRecorder) GetLicense() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicense", reflect.TypeOf((*MockServicesAPI)(nil).GetLicense))
}
// GetLogger mocks base method.
func (m *MockServicesAPI) GetLogger() mlog.LoggerIFace {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLogger")
ret0, _ := ret[0].(mlog.LoggerIFace)
return ret0
}
// GetLogger indicates an expected call of GetLogger.
func (mr *MockServicesAPIMockRecorder) GetLogger() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogger", reflect.TypeOf((*MockServicesAPI)(nil).GetLogger))
}
// GetMasterDB mocks base method.
func (m *MockServicesAPI) GetMasterDB() (*sql.DB, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMasterDB")
ret0, _ := ret[0].(*sql.DB)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMasterDB indicates an expected call of GetMasterDB.
func (mr *MockServicesAPIMockRecorder) GetMasterDB() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMasterDB", reflect.TypeOf((*MockServicesAPI)(nil).GetMasterDB))
}
// GetPreferencesForUser mocks base method.
func (m *MockServicesAPI) GetPreferencesForUser(arg0 string) (model.Preferences, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPreferencesForUser", arg0)
ret0, _ := ret[0].(model.Preferences)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPreferencesForUser indicates an expected call of GetPreferencesForUser.
func (mr *MockServicesAPIMockRecorder) GetPreferencesForUser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).GetPreferencesForUser), arg0)
}
// GetTeamMember mocks base method.
func (m *MockServicesAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTeamMember", arg0, arg1)
ret0, _ := ret[0].(*model.TeamMember)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTeamMember indicates an expected call of GetTeamMember.
func (mr *MockServicesAPIMockRecorder) GetTeamMember(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMember", reflect.TypeOf((*MockServicesAPI)(nil).GetTeamMember), arg0, arg1)
}
// GetUserByEmail mocks base method.
func (m *MockServicesAPI) GetUserByEmail(arg0 string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByEmail", arg0)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByEmail indicates an expected call of GetUserByEmail.
func (mr *MockServicesAPIMockRecorder) GetUserByEmail(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockServicesAPI)(nil).GetUserByEmail), arg0)
}
// GetUserByID mocks base method.
func (m *MockServicesAPI) GetUserByID(arg0 string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByID", arg0)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByID indicates an expected call of GetUserByID.
func (mr *MockServicesAPIMockRecorder) GetUserByID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockServicesAPI)(nil).GetUserByID), arg0)
}
// GetUserByUsername mocks base method.
func (m *MockServicesAPI) GetUserByUsername(arg0 string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByUsername", arg0)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByUsername indicates an expected call of GetUserByUsername.
func (mr *MockServicesAPIMockRecorder) GetUserByUsername(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockServicesAPI)(nil).GetUserByUsername), arg0)
}
// GetUsersFromProfiles mocks base method.
func (m *MockServicesAPI) GetUsersFromProfiles(arg0 *model.UserGetOptions) ([]*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUsersFromProfiles", arg0)
ret0, _ := ret[0].([]*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUsersFromProfiles indicates an expected call of GetUsersFromProfiles.
func (mr *MockServicesAPIMockRecorder) GetUsersFromProfiles(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersFromProfiles", reflect.TypeOf((*MockServicesAPI)(nil).GetUsersFromProfiles), arg0)
}
// HasPermissionTo mocks base method.
func (m *MockServicesAPI) HasPermissionTo(arg0 string, arg1 *model.Permission) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HasPermissionTo", arg0, arg1)
ret0, _ := ret[0].(bool)
return ret0
}
// HasPermissionTo indicates an expected call of HasPermissionTo.
func (mr *MockServicesAPIMockRecorder) HasPermissionTo(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionTo", reflect.TypeOf((*MockServicesAPI)(nil).HasPermissionTo), arg0, arg1)
}
// HasPermissionToChannel mocks base method.
func (m *MockServicesAPI) HasPermissionToChannel(arg0, arg1 string, arg2 *model.Permission) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HasPermissionToChannel", arg0, arg1, arg2)
ret0, _ := ret[0].(bool)
return ret0
}
// HasPermissionToChannel indicates an expected call of HasPermissionToChannel.
func (mr *MockServicesAPIMockRecorder) HasPermissionToChannel(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToChannel", reflect.TypeOf((*MockServicesAPI)(nil).HasPermissionToChannel), arg0, arg1, arg2)
}
// HasPermissionToTeam mocks base method.
func (m *MockServicesAPI) HasPermissionToTeam(arg0, arg1 string, arg2 *model.Permission) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HasPermissionToTeam", arg0, arg1, arg2)
ret0, _ := ret[0].(bool)
return ret0
}
// HasPermissionToTeam indicates an expected call of HasPermissionToTeam.
func (mr *MockServicesAPIMockRecorder) HasPermissionToTeam(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPermissionToTeam", reflect.TypeOf((*MockServicesAPI)(nil).HasPermissionToTeam), arg0, arg1, arg2)
}
// KVSetWithOptions mocks base method.
func (m *MockServicesAPI) KVSetWithOptions(arg0 string, arg1 []byte, arg2 model.PluginKVSetOptions) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KVSetWithOptions", arg0, arg1, arg2)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// KVSetWithOptions indicates an expected call of KVSetWithOptions.
func (mr *MockServicesAPIMockRecorder) KVSetWithOptions(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KVSetWithOptions", reflect.TypeOf((*MockServicesAPI)(nil).KVSetWithOptions), arg0, arg1, arg2)
}
// PublishPluginClusterEvent mocks base method.
func (m *MockServicesAPI) PublishPluginClusterEvent(arg0 model.PluginClusterEvent, arg1 model.PluginClusterEventSendOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishPluginClusterEvent", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// PublishPluginClusterEvent indicates an expected call of PublishPluginClusterEvent.
func (mr *MockServicesAPIMockRecorder) PublishPluginClusterEvent(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishPluginClusterEvent", reflect.TypeOf((*MockServicesAPI)(nil).PublishPluginClusterEvent), arg0, arg1)
}
// PublishWebSocketEvent mocks base method.
func (m *MockServicesAPI) PublishWebSocketEvent(arg0 string, arg1 map[string]interface{}, arg2 *model.WebsocketBroadcast) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "PublishWebSocketEvent", arg0, arg1, arg2)
}
// PublishWebSocketEvent indicates an expected call of PublishWebSocketEvent.
func (mr *MockServicesAPIMockRecorder) PublishWebSocketEvent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebSocketEvent", reflect.TypeOf((*MockServicesAPI)(nil).PublishWebSocketEvent), arg0, arg1, arg2)
}
// RegisterRouter mocks base method.
func (m *MockServicesAPI) RegisterRouter(arg0 *mux.Router) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RegisterRouter", arg0)
}
// RegisterRouter indicates an expected call of RegisterRouter.
func (mr *MockServicesAPIMockRecorder) RegisterRouter(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRouter", reflect.TypeOf((*MockServicesAPI)(nil).RegisterRouter), arg0)
}
// UpdatePreferencesForUser mocks base method.
func (m *MockServicesAPI) UpdatePreferencesForUser(arg0 string, arg1 model.Preferences) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePreferencesForUser", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdatePreferencesForUser indicates an expected call of UpdatePreferencesForUser.
func (mr *MockServicesAPIMockRecorder) UpdatePreferencesForUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).UpdatePreferencesForUser), arg0, arg1)
}
// UpdateUser mocks base method.
func (m *MockServicesAPI) UpdateUser(arg0 *model.User) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", arg0)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockServicesAPIMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockServicesAPI)(nil).UpdateUser), arg0)
}

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

@@ -0,0 +1,53 @@
// 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/model (interfaces: PropValueResolver)
// 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"
)
// MockPropValueResolver is a mock of PropValueResolver interface.
type MockPropValueResolver struct {
ctrl *gomock.Controller
recorder *MockPropValueResolverMockRecorder
}
// MockPropValueResolverMockRecorder is the mock recorder for MockPropValueResolver.
type MockPropValueResolverMockRecorder struct {
mock *MockPropValueResolver
}
// NewMockPropValueResolver creates a new mock instance.
func NewMockPropValueResolver(ctrl *gomock.Controller) *MockPropValueResolver {
mock := &MockPropValueResolver{ctrl: ctrl}
mock.recorder = &MockPropValueResolverMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPropValueResolver) EXPECT() *MockPropValueResolverMockRecorder {
return m.recorder
}
// GetUserByID mocks base method.
func (m *MockPropValueResolver) GetUserByID(arg0 string) (*model.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByID", arg0)
ret0, _ := ret[0].(*model.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByID indicates an expected call of GetUserByID.
func (mr *MockPropValueResolverMockRecorder) GetUserByID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockPropValueResolver)(nil).GetUserByID), arg0)
}

84
server/boards/model/notification.go Обычный файл
Просмотреть файл

@@ -0,0 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"time"
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
)
// NotificationHint provides a hint that a block has been modified and has subscribers that
// should be notified.
// swagger:model
type NotificationHint struct {
// BlockType is the block type of the entity (e.g. board, card) that was updated
// required: true
BlockType BlockType `json:"block_type"`
// BlockID is id of the entity that was updated
// required: true
BlockID string `json:"block_id"`
// ModifiedByID is the id of the user who made the block change
ModifiedByID string `json:"modified_by_id"`
// CreatedAt is the timestamp this notification hint was created in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"create_at"`
// NotifyAt is the timestamp this notification should be scheduled in miliseconds since the current epoch
// required: true
NotifyAt int64 `json:"notify_at"`
}
func (s *NotificationHint) IsValid() error {
if s == nil {
return ErrInvalidNotificationHint{"cannot be nil"}
}
if s.BlockID == "" {
return ErrInvalidNotificationHint{"missing block id"}
}
if s.BlockType == "" {
return ErrInvalidNotificationHint{"missing block type"}
}
if s.ModifiedByID == "" {
return ErrInvalidNotificationHint{"missing modified_by id"}
}
return nil
}
func (s *NotificationHint) Copy() *NotificationHint {
return &NotificationHint{
BlockType: s.BlockType,
BlockID: s.BlockID,
ModifiedByID: s.ModifiedByID,
CreateAt: s.CreateAt,
NotifyAt: s.NotifyAt,
}
}
func (s *NotificationHint) LogClone() interface{} {
return struct {
BlockType BlockType `json:"block_type"`
BlockID string `json:"block_id"`
ModifiedByID string `json:"modified_by_id"`
CreateAt string `json:"create_at"`
NotifyAt string `json:"notify_at"`
}{
BlockType: s.BlockType,
BlockID: s.BlockID,
ModifiedByID: s.ModifiedByID,
CreateAt: utils.TimeFromMillis(s.CreateAt).Format(time.StampMilli),
NotifyAt: utils.TimeFromMillis(s.NotifyAt).Format(time.StampMilli),
}
}
type ErrInvalidNotificationHint struct {
msg string
}
func (e ErrInvalidNotificationHint) Error() string {
return e.msg
}

28
server/boards/model/permission.go Обычный файл
Просмотреть файл

@@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
var (
PermissionViewTeam = mm_model.PermissionViewTeam
PermissionManageTeam = mm_model.PermissionManageTeam
PermissionManageSystem = mm_model.PermissionManageSystem
PermissionReadChannel = mm_model.PermissionReadChannel
PermissionCreatePost = mm_model.PermissionCreatePost
PermissionViewMembers = mm_model.PermissionViewMembers
PermissionCreatePublicChannel = mm_model.PermissionCreatePublicChannel
PermissionCreatePrivateChannel = mm_model.PermissionCreatePrivateChannel
PermissionManageBoardType = &mm_model.Permission{Id: "manage_board_type", Name: "", Description: "", Scope: ""}
PermissionDeleteBoard = &mm_model.Permission{Id: "delete_board", Name: "", Description: "", Scope: ""}
PermissionViewBoard = &mm_model.Permission{Id: "view_board", Name: "", Description: "", Scope: ""}
PermissionManageBoardRoles = &mm_model.Permission{Id: "manage_board_roles", Name: "", Description: "", Scope: ""}
PermissionShareBoard = &mm_model.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
PermissionManageBoardCards = &mm_model.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
PermissionManageBoardProperties = &mm_model.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
PermissionCommentBoardCards = &mm_model.Permission{Id: "comment_board_cards", Name: "", Description: "", Scope: ""}
PermissionDeleteOthersComments = &mm_model.Permission{Id: "delete_others_comments", Name: "", Description: "", Scope: ""}
)

273
server/boards/model/properties.go Обычный файл
Просмотреть файл

@@ -0,0 +1,273 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate mockgen -copyright_file=../../copyright.txt -destination=mocks/propValueResolverMock.go -package mocks . PropValueResolver
package model
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
var ErrInvalidBoardBlock = errors.New("invalid board block")
var ErrInvalidPropSchema = errors.New("invalid property schema")
var ErrInvalidProperty = errors.New("invalid property")
var ErrInvalidPropertyValue = errors.New("invalid property value")
var ErrInvalidPropertyValueType = errors.New("invalid property value type")
var ErrInvalidDate = errors.New("invalid date property")
// PropValueResolver allows PropDef.GetValue to further decode property values, such as
// looking up usernames from ids.
type PropValueResolver interface {
GetUserByID(userID string) (*User, error)
}
// BlockProperties is a map of Prop's keyed by property id.
type BlockProperties map[string]BlockProp
// BlockProp represent a property attached to a block (typically a card).
type BlockProp struct {
ID string `json:"id"`
Index int `json:"index"`
Name string `json:"name"`
Value string `json:"value"`
}
// PropSchema is a map of PropDef's keyed by property id.
type PropSchema map[string]PropDef
// PropDefOption represents an option within a property definition.
type PropDefOption struct {
ID string `json:"id"`
Index int `json:"index"`
Color string `json:"color"`
Value string `json:"value"`
}
// PropDef represents a property definition as defined in a board's Fields member.
type PropDef struct {
ID string `json:"id"`
Index int `json:"index"`
Name string `json:"name"`
Type string `json:"type"`
Options map[string]PropDefOption `json:"options"`
}
// GetValue resolves the value of a property if the passed value is an ID for an option,
// otherwise returns the original value.
func (pd PropDef) GetValue(v interface{}, resolver PropValueResolver) (string, error) {
switch pd.Type {
case "select":
// v is the id of an option
id, ok := v.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
opt, ok := pd.Options[id]
if !ok {
return "", ErrInvalidPropertyValue
}
return strings.ToUpper(opt.Value), nil
case "date":
// v is a JSON string
date, ok := v.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
return pd.ParseDate(date)
case "person":
// v is a userid
userID, ok := v.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
if resolver != nil {
user, err := resolver.GetUserByID(userID)
if err != nil {
return "", err
}
if user == nil {
return userID, nil
}
return user.Username, nil
}
return userID, nil
case "multiPerson":
// v is a slice of user IDs
userIDs, ok := v.([]interface{})
if !ok {
return "", fmt.Errorf("multiPerson property type: %w", ErrInvalidPropertyValueType)
}
if resolver != nil {
usernames := make([]string, len(userIDs))
for i, userIDInterface := range userIDs {
userID := userIDInterface.(string)
user, err := resolver.GetUserByID(userID)
if err != nil {
return "", err
}
if user == nil {
usernames[i] = userID
} else {
usernames[i] = user.Username
}
}
return strings.Join(usernames, ", "), nil
}
case "multiSelect":
// v is a slice of strings containing option ids
ms, ok := v.([]interface{})
if !ok {
return "", ErrInvalidPropertyValueType
}
var sb strings.Builder
prefix := ""
for _, optid := range ms {
id, ok := optid.(string)
if !ok {
return "", ErrInvalidPropertyValueType
}
opt, ok := pd.Options[id]
if !ok {
return "", ErrInvalidPropertyValue
}
sb.WriteString(prefix)
prefix = ", "
sb.WriteString(strings.ToUpper(opt.Value))
}
return sb.String(), nil
}
return fmt.Sprintf("%v", v), nil
}
func (pd PropDef) ParseDate(s string) (string, error) {
// s is a JSON snippet of the form: {"from":1642161600000, "to":1642161600000} in milliseconds UTC
// The UI does not yet support date ranges.
var m map[string]int64
if err := json.Unmarshal([]byte(s), &m); err != nil {
return s, err
}
tsFrom, ok := m["from"]
if !ok {
return s, ErrInvalidDate
}
date := utils.GetTimeForMillis(tsFrom).Format("January 02, 2006")
tsTo, ok := m["to"]
if ok {
date += " -> " + utils.GetTimeForMillis(tsTo).Format("January 02, 2006")
}
return date, nil
}
// ParsePropertySchema parses a board block's `Fields` to extract the properties
// schema for all cards within the board.
// The result is provided as a map for quick lookup, and the original order is
// preserved via the `Index` field.
func ParsePropertySchema(board *Board) (PropSchema, error) {
schema := make(map[string]PropDef)
for i, prop := range board.CardProperties {
pd := PropDef{
ID: getMapString("id", prop),
Index: i,
Name: getMapString("name", prop),
Type: getMapString("type", prop),
Options: make(map[string]PropDefOption),
}
optsIface, ok := prop["options"]
if ok {
opts, ok := optsIface.([]interface{})
if !ok {
return nil, ErrInvalidPropSchema
}
for j, propOptIface := range opts {
propOpt, ok := propOptIface.(map[string]interface{})
if !ok {
return nil, ErrInvalidPropSchema
}
po := PropDefOption{
ID: getMapString("id", propOpt),
Index: j,
Value: getMapString("value", propOpt),
Color: getMapString("color", propOpt),
}
pd.Options[po.ID] = po
}
}
schema[pd.ID] = pd
}
return schema, nil
}
func getMapString(key string, m map[string]interface{}) string {
iface, ok := m[key]
if !ok {
return ""
}
s, ok := iface.(string)
if !ok {
return ""
}
return s
}
// ParseProperties parses a block's `Fields` to extract the properties. Properties typically exist on
// card blocks. A resolver can optionally be provided to fetch usernames for `person` prop type.
func ParseProperties(block *Block, schema PropSchema, resolver PropValueResolver) (BlockProperties, error) {
props := make(map[string]BlockProp)
if block == nil {
return props, nil
}
// `properties` contains a map (untyped at this point).
propsIface, ok := block.Fields["properties"]
if !ok {
return props, nil // this is expected for blocks that don't have any properties.
}
blockProps, ok := propsIface.(map[string]interface{})
if !ok {
return props, fmt.Errorf("`properties` field wrong type: %w", ErrInvalidProperty)
}
if len(blockProps) == 0 {
return props, nil
}
for k, v := range blockProps {
s := fmt.Sprintf("%v", v)
prop := BlockProp{
ID: k,
Name: k,
Value: s,
}
def, ok := schema[k]
if ok {
val, err := def.GetValue(v, resolver)
if err != nil {
return props, fmt.Errorf("could not parse property value (%s): %w", fmt.Sprintf("%v", v), err)
}
prop.Name = def.Name
prop.Value = val
prop.Index = def.Index
}
props[k] = prop
}
return props, nil
}

164
server/boards/model/properties_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,164 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
type MockResolver struct{}
func (r MockResolver) GetUserByID(userID string) (*User, error) {
if userID == "user_id_1" {
return &User{
ID: "user_id_1",
Username: "username_1",
}, nil
} else if userID == "user_id_2" {
return &User{
ID: "user_id_2",
Username: "username_2",
}, nil
}
return nil, nil
}
func Test_parsePropertySchema(t *testing.T) {
board := &Board{
ID: utils.NewID(utils.IDTypeBoard),
Title: "Test Board",
TeamID: utils.NewID(utils.IDTypeTeam),
}
err := json.Unmarshal([]byte(cardPropertiesExample), &board.CardProperties)
require.NoError(t, err)
t.Run("parse schema", func(t *testing.T) {
schema, err := ParsePropertySchema(board)
require.NoError(t, err)
assert.Len(t, schema, 6)
prop, ok := schema["7c212e78-9345-4c60-81b5-0b0e37ce463f"]
require.True(t, ok)
assert.Equal(t, "select", prop.Type)
assert.Equal(t, "Type", prop.Name)
assert.Len(t, prop.Options, 3)
prop, ok = schema["a8spou7if43eo1rqzb9qeq488so"]
require.True(t, ok)
assert.Equal(t, "date", prop.Type)
assert.Equal(t, "MyDate", prop.Name)
assert.Empty(t, prop.Options)
})
}
func Test_GetValue(t *testing.T) {
resolver := MockResolver{}
propDef := PropDef{
Type: "multiPerson",
}
value, err := propDef.GetValue([]interface{}{"user_id_1", "user_id_2"}, resolver)
require.NoError(t, err)
require.Equal(t, "username_1, username_2", value)
// trying with only user
value, err = propDef.GetValue([]interface{}{"user_id_1"}, resolver)
require.NoError(t, err)
require.Equal(t, "username_1", value)
// trying with unknown user
value, err = propDef.GetValue([]interface{}{"user_id_1", "user_id_unknown"}, resolver)
require.NoError(t, err)
require.Equal(t, "username_1, user_id_unknown", value)
// trying with multiple unknown users
value, err = propDef.GetValue([]interface{}{"michael_scott", "jim_halpert"}, resolver)
require.NoError(t, err)
require.Equal(t, "michael_scott, jim_halpert", value)
}
const (
cardPropertiesExample = `[
{
"id":"7c212e78-9345-4c60-81b5-0b0e37ce463f",
"name":"Type",
"options":[
{
"color":"propColorYellow",
"id":"31da50ca-f1a9-4d21-8636-17dc387c1a23",
"value":"Ad Hoc"
},
{
"color":"propColorBlue",
"id":"def6317c-ec11-410d-8a6b-ea461320f392",
"value":"Standup"
},
{
"color":"propColorPurple",
"id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7",
"value":"Weekly Sync"
}
],
"type":"select"
},
{
"id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4",
"name":"Summary",
"options":[],
"type":"text"
},
{
"id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c",
"name":"Color",
"options":[
{
"color":"propColorDefault",
"id":"efb0c783-f9ea-4938-8b86-9cf425296cd1",
"value":"RED"
},
{
"color":"propColorDefault",
"id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311",
"value":"GREEN"
},
{
"color":"propColorDefault",
"id":"a05bdc80-bd90-45b0-8805-a7e77a4884be",
"value":"BLUE"
}
],
"type":"select"
},
{
"id":"aawg1s8rxq8o1bbksxmsmpsdd3r",
"name":"MyTextProp",
"options":[],
"type":"text"
},
{
"id":"awdwfigo4kse63bdfp56mzhip6w",
"name":"MyCheckBox",
"options":[],
"type":"checkbox"
},
{
"id":"a8spou7if43eo1rqzb9qeq488so",
"name":"MyDate",
"options":[],
"type":"date"
}
]`
)

95
server/boards/model/services_api.go Обычный файл
Просмотреть файл

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//go:generate mockgen --build_flags= -copyright_file=../../copyright.txt -destination=mocks/mockservicesapi.go -package mocks . ServicesAPI
package model
import (
"database/sql"
"github.com/gorilla/mux"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
botUsername = "boards"
botDisplayname = "Boards"
botDescription = "Created by Boards plugin."
)
var FocalboardBot = &mm_model.Bot{
Username: botUsername,
DisplayName: botDisplayname,
Description: botDescription,
OwnerId: SystemUserID,
}
type ServicesAPI interface {
// Channels service
GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error)
GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error)
GetChannelByID(channelID string) (*mm_model.Channel, error)
GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error)
GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error)
// Post service
CreatePost(post *mm_model.Post) (*mm_model.Post, error)
// User service
GetUserByID(userID string) (*mm_model.User, error)
GetUserByUsername(name string) (*mm_model.User, error)
GetUserByEmail(email string) (*mm_model.User, error)
UpdateUser(user *mm_model.User) (*mm_model.User, error)
GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error)
// Team service
GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error)
CreateMember(teamID string, userID string) (*mm_model.TeamMember, error)
// Permissions service
HasPermissionTo(userID string, permission *mm_model.Permission) bool
HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool
HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool
// Bot service
EnsureBot(bot *mm_model.Bot) (string, error)
// License service
GetLicense() *mm_model.License
// FileInfoStore service
GetFileInfo(fileID string) (*mm_model.FileInfo, error)
// Cluster service
PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast)
PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error
// Cloud service
GetCloudLimits() (*mm_model.ProductLimits, error)
// Config service
GetConfig() *mm_model.Config
// Logger service
GetLogger() mlog.LoggerIFace
// KVStore service
KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error)
// Store service
GetMasterDB() (*sql.DB, error)
// System service
GetDiagnosticID() string
// Router service
RegisterRouter(sub *mux.Router)
// Preferences services
GetPreferencesForUser(userID string) (mm_model.Preferences, error)
UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error
DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error
}

39
server/boards/model/sharing.go Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
// Sharing is sharing information for a root block
// swagger:model
type Sharing struct {
// ID of the root block
// required: true
ID string `json:"id"`
// Is sharing enabled
// required: true
Enabled bool `json:"enabled"`
// Access token
// required: true
Token string `json:"token"`
// ID of the user who last modified this
// required: true
ModifiedBy string `json:"modifiedBy"`
// Updated time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"update_at,omitempty"`
}
func SharingFromJSON(data io.Reader) Sharing {
var sharing Sharing
_ = json.NewDecoder(data).Decode(&sharing)
return sharing
}

106
server/boards/model/subscription.go Обычный файл
Просмотреть файл

@@ -0,0 +1,106 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
SubTypeUser = "user"
SubTypeChannel = "channel"
)
type SubscriberType string
func (st SubscriberType) IsValid() bool {
switch st {
case SubTypeUser, SubTypeChannel:
return true
}
return false
}
// Subscription is a subscription to a board, card, etc, for a user or channel.
// swagger:model
type Subscription struct {
// BlockType is the block type of the entity (e.g. board, card) subscribed to
// required: true
BlockType BlockType `json:"blockType"`
// BlockID is id of the entity being subscribed to
// required: true
BlockID string `json:"blockId"`
// SubscriberType is the type of the entity (e.g. user, channel) that is subscribing
// required: true
SubscriberType SubscriberType `json:"subscriberType"`
// SubscriberID is the id of the entity that is subscribing
// required: true
SubscriberID string `json:"subscriberId"`
// NotifiedAt is the timestamp of the last notification sent for this subscription
// required: true
NotifiedAt int64 `json:"notifiedAt,omitempty"`
// CreatedAt is the timestamp this subscription was created in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"createAt"`
// DeleteAt is the timestamp this subscription was deleted in miliseconds since the current epoch, or zero if not deleted
// required: true
DeleteAt int64 `json:"deleteAt"`
}
func (s *Subscription) IsValid() error {
if s == nil {
return ErrInvalidSubscription{"cannot be nil"}
}
if s.BlockID == "" {
return ErrInvalidSubscription{"missing block id"}
}
if s.BlockType == "" {
return ErrInvalidSubscription{"missing block type"}
}
if s.SubscriberID == "" {
return ErrInvalidSubscription{"missing subscriber id"}
}
if !s.SubscriberType.IsValid() {
return ErrInvalidSubscription{"invalid subscriber type"}
}
return nil
}
func SubscriptionFromJSON(data io.Reader) (*Subscription, error) {
var subscription Subscription
if err := json.NewDecoder(data).Decode(&subscription); err != nil {
return nil, err
}
return &subscription, nil
}
type ErrInvalidSubscription struct {
msg string
}
func (e ErrInvalidSubscription) Error() string {
return e.msg
}
// Subscriber is an entity (e.g. user, channel) that can subscribe to events from boards, cards, etc
// swagger:model
type Subscriber struct {
// SubscriberType is the type of the entity (e.g. user, channel) that is subscribing
// required: true
SubscriberType SubscriberType `json:"subscriber_type"`
// SubscriberID is the id of the entity that is subscribing
// required: true
SubscriberID string `json:"subscriber_id"`
// NotifiedAt is the timestamp this subscriber was last notified
NotifiedAt int64 `json:"notified_at"`
}

49
server/boards/model/team.go Обычный файл
Просмотреть файл

@@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
// Team is information global to a team
// swagger:model
type Team struct {
// ID of the team
// required: true
ID string `json:"id"`
// Title of the team
// required: false
Title string `json:"title"`
// Token required to register new users
// required: true
SignupToken string `json:"signupToken"`
// Team settings
// required: false
Settings map[string]interface{} `json:"settings"`
// ID of user who last modified this
// required: true
ModifiedBy string `json:"modifiedBy"`
// Updated time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"updateAt"`
}
func TeamFromJSON(data io.Reader) *Team {
var team *Team
_ = json.NewDecoder(data).Decode(&team)
return team
}
func TeamsFromJSON(data io.Reader) []*Team {
var teams []*Team
_ = json.NewDecoder(data).Decode(&teams)
return teams
}

106
server/boards/model/user.go Обычный файл
Просмотреть файл

@@ -0,0 +1,106 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
SingleUser = "single-user"
GlobalTeamID = "0"
SystemUserID = "system"
PreferencesCategoryFocalboard = "focalboard"
)
// User is a user
// swagger:model
type User struct {
// The user ID
// required: true
ID string `json:"id"`
// The user name
// required: true
Username string `json:"username"`
// The user's email
// required: true
Email string `json:"-"`
// The user's nickname
Nickname string `json:"nickname"`
// The user's first name
FirstName string `json:"firstname"`
// The user's last name
LastName string `json:"lastname"`
// swagger:ignore
Password string `json:"-"`
// swagger:ignore
MfaSecret string `json:"-"`
// swagger:ignore
AuthService string `json:"-"`
// swagger:ignore
AuthData string `json:"-"`
// Created time in miliseconds since the current epoch
// required: true
CreateAt int64 `json:"create_at,omitempty"`
// Updated time in miliseconds since the current epoch
// required: true
UpdateAt int64 `json:"update_at,omitempty"`
// Deleted time in miliseconds since the current epoch, set to indicate user is deleted
// required: true
DeleteAt int64 `json:"delete_at"`
// If the user is a bot or not
// required: true
IsBot bool `json:"is_bot"`
// If the user is a guest or not
// required: true
IsGuest bool `json:"is_guest"`
// Special Permissions the user may have
Permissions []string `json:"permissions,omitempty"`
Roles string `json:"roles"`
}
// UserPreferencesPatch is a user property patch
// swagger:model
type UserPreferencesPatch struct {
// The user preference updated fields
// required: false
UpdatedFields map[string]string `json:"updatedFields"`
// The user preference removed fields
// required: false
DeletedFields []string `json:"deletedFields"`
}
type Session struct {
ID string `json:"id"`
Token string `json:"token"`
UserID string `json:"user_id"`
AuthService string `json:"authService"`
Props map[string]interface{} `json:"props"`
CreateAt int64 `json:"create_at,omitempty"`
UpdateAt int64 `json:"update_at,omitempty"`
}
func UserFromJSON(data io.Reader) (*User, error) {
var user User
if err := json.NewDecoder(data).Decode(&user); err != nil {
return nil, err
}
return &user, nil
}

25
server/boards/model/util.go Обычный файл
Просмотреть файл

@@ -0,0 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"time"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
// GetMillis is a convenience method to get milliseconds since epoch.
func GetMillis() int64 {
return mm_model.GetMillis()
}
// GetMillisForTime is a convenience method to get milliseconds since epoch for provided Time.
func GetMillisForTime(thisTime time.Time) int64 {
return mm_model.GetMillisForTime(thisTime)
}
// GetTimeForMillis is a convenience method to get time.Time for milliseconds since epoch.
func GetTimeForMillis(millis int64) time.Time {
return mm_model.GetTimeForMillis(millis)
}

67
server/boards/model/version.go Обычный файл
Просмотреть файл

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
// This is a list of all the current versions including any patches.
// It should be maintained in chronological order with most current
// release at the front of the list.
var versions = []string{
"7.9.0",
"7.8.0",
"7.7.0",
"7.6.0",
"7.5.0",
"7.4.0",
"7.3.0",
"7.2.0",
"7.0.0",
"0.16.0",
"0.15.0",
"0.14.0",
"0.12.0",
"0.11.0",
"0.10.0",
"0.9.4",
"0.9.3",
"0.9.2",
"0.9.1",
"0.9.0",
"0.8.2",
"0.8.1",
"0.8.0",
"0.7.3",
"0.7.2",
"0.7.1",
"0.7.0",
"0.6.7",
"0.6.6",
"0.6.5",
"0.6.2",
"0.6.1",
"0.6.0",
"0.5.0",
}
var (
CurrentVersion = versions[0]
BuildNumber string
BuildDate string
BuildHash string
Edition string
)
// LogServerInfo logs information about the server instance.
func LogServerInfo(logger mlog.LoggerIFace) {
logger.Info("Focalboard server",
mlog.String("version", CurrentVersion),
mlog.String("edition", Edition),
mlog.String("build_number", BuildNumber),
mlog.String("build_date", BuildDate),
mlog.String("build_hash", BuildHash),
)
}