// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "errors" "fmt" "github.com/mattermost/mattermost-server/v6/shared/mlog" mmModel "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/utils" ) var ErrNilPluginAPI = errors.New("server not running in plugin mode") // GetBoardsCloudLimits returns the limits of the server, and an empty // limits struct if there are no limits set. func (a *App) GetBoardsCloudLimits() (*model.BoardsCloudLimits, error) { if !a.IsCloud() { return &model.BoardsCloudLimits{}, nil } productLimits, err := a.store.GetCloudLimits() if err != nil { return nil, err } usedCards, err := a.store.GetUsedCardsCount() if err != nil { return nil, err } cardLimitTimestamp, err := a.store.GetCardLimitTimestamp() if err != nil { return nil, err } boardsCloudLimits := &model.BoardsCloudLimits{ UsedCards: usedCards, CardLimitTimestamp: cardLimitTimestamp, } if productLimits != nil && productLimits.Boards != nil { if productLimits.Boards.Cards != nil { boardsCloudLimits.Cards = *productLimits.Boards.Cards } if productLimits.Boards.Views != nil { boardsCloudLimits.Views = *productLimits.Boards.Views } } return boardsCloudLimits, nil } // IsCloud returns true if the server is running as a plugin in a // cloud licensed server. func (a *App) IsCloud() bool { return utils.IsCloudLicense(a.store.GetLicense()) } // IsCloudLimited returns true if the server is running in cloud mode // and the card limit has been set. func (a *App) IsCloudLimited() bool { return a.CardLimit() != 0 && a.IsCloud() } // SetCloudLimits sets the limits of the server. func (a *App) SetCloudLimits(limits *mmModel.ProductLimits) error { oldCardLimit := a.CardLimit() // if the limit object doesn't come complete, we assume limits are // being disabled cardLimit := 0 if limits != nil && limits.Boards != nil && limits.Boards.Cards != nil { cardLimit = *limits.Boards.Cards } if oldCardLimit != cardLimit { a.SetCardLimit(cardLimit) return a.doUpdateCardLimitTimestamp() } return nil } // doUpdateCardLimitTimestamp performs the update without running any // checks. func (a *App) doUpdateCardLimitTimestamp() error { cardLimitTimestamp, err := a.store.UpdateCardLimitTimestamp(a.CardLimit()) if err != nil { return err } a.wsAdapter.BroadcastCardLimitTimestampChange(cardLimitTimestamp) return nil } // UpdateCardLimitTimestamp checks if the server is a cloud instance // with limits applied, and if that's true, recalculates the card // limit timestamp and propagates the new one to the connected // clients. func (a *App) UpdateCardLimitTimestamp() error { if !a.IsCloudLimited() { return nil } return a.doUpdateCardLimitTimestamp() } // getTemplateMapForBlocks gets all board ids for the blocks, and // builds a map with the board IDs as the key and their isTemplate // field as the value. func (a *App) getTemplateMapForBlocks(blocks []model.Block) (map[string]bool, error) { boardMap := map[string]*model.Board{} for _, block := range blocks { if _, ok := boardMap[block.BoardID]; !ok { board, err := a.store.GetBoard(block.BoardID) if err != nil { return nil, err } boardMap[block.BoardID] = board } } templateMap := map[string]bool{} for boardID, board := range boardMap { templateMap[boardID] = board.IsTemplate } return templateMap, nil } // ApplyCloudLimits takes a set of blocks and, if the server is cloud // limited, limits those that are outside of the card limit and don't // belong to a template. func (a *App) ApplyCloudLimits(blocks []model.Block) ([]model.Block, error) { // if there is no limit currently being applied, return if !a.IsCloudLimited() { return blocks, nil } cardLimitTimestamp, err := a.store.GetCardLimitTimestamp() if err != nil { return nil, err } templateMap, err := a.getTemplateMapForBlocks(blocks) if err != nil { return nil, err } limitedBlocks := make([]model.Block, len(blocks)) for i, block := range blocks { // if the block belongs to a template, it will never be // limited if isTemplate, ok := templateMap[block.BoardID]; ok && isTemplate { limitedBlocks[i] = block continue } if block.ShouldBeLimited(cardLimitTimestamp) { limitedBlocks[i] = block.GetLimited() } else { limitedBlocks[i] = block } } return limitedBlocks, nil } // ContainsLimitedBlocks checks if a list of blocks contain any block // that references a limited card. func (a *App) ContainsLimitedBlocks(blocks []model.Block) (bool, error) { cardLimitTimestamp, err := a.store.GetCardLimitTimestamp() if err != nil { return false, err } if cardLimitTimestamp == 0 { return false, nil } cards := []model.Block{} cardIDMap := map[string]bool{} for _, block := range blocks { switch block.Type { case model.TypeCard: cards = append(cards, block) default: cardIDMap[block.ParentID] = true } } cardIDs := []string{} // if the card is already present on the set, we don't need to // fetch it from the database for cardID := range cardIDMap { alreadyPresent := false for _, card := range cards { if card.ID == cardID { alreadyPresent = true break } } if !alreadyPresent { cardIDs = append(cardIDs, cardID) } } if len(cardIDs) > 0 { fetchedCards, fErr := a.store.GetBlocksByIDs(cardIDs) if fErr != nil { return false, fErr } cards = append(cards, fetchedCards...) } templateMap, err := a.getTemplateMapForBlocks(cards) if err != nil { return false, err } for _, card := range cards { isTemplate, ok := templateMap[card.BoardID] if !ok { return false, newErrBoardNotFoundInTemplateMap(card.BoardID) } // if the block belongs to a template, it will never be // limited if isTemplate { continue } if card.ShouldBeLimited(cardLimitTimestamp) { return true, nil } } return false, nil } type errBoardNotFoundInTemplateMap struct { id string } func newErrBoardNotFoundInTemplateMap(id string) *errBoardNotFoundInTemplateMap { return &errBoardNotFoundInTemplateMap{id} } func (eb *errBoardNotFoundInTemplateMap) Error() string { return fmt.Sprintf("board %q not found in template map", eb.id) } func (a *App) NotifyPortalAdminsUpgradeRequest(teamID string) error { if a.pluginAPI == nil { return ErrNilPluginAPI } team, err := a.store.GetTeam(teamID) if err != nil { return err } var ofWhat string if team == nil { ofWhat = "your organization" } else { ofWhat = team.Title } message := fmt.Sprintf("A member of %s has notified you to upgrade this workspace before the trial ends.", ofWhat) page := 0 getUsersOptions := &mmModel.UserGetOptions{ Active: true, Role: mmModel.SystemAdminRoleId, PerPage: 50, Page: page, } for ; true; page++ { getUsersOptions.Page = page systemAdmins, appErr := a.pluginAPI.GetUsers(getUsersOptions) if appErr != nil { a.logger.Error("failed to fetch system admins", mlog.Int("page_size", getUsersOptions.PerPage), mlog.Int("page", page), mlog.Err(appErr)) return appErr } if len(systemAdmins) == 0 { break } receiptUserIDs := []string{} for _, systemAdmin := range systemAdmins { receiptUserIDs = append(receiptUserIDs, systemAdmin.Id) } if err := a.store.SendMessage(message, "custom_cloud_upgrade_nudge", receiptUserIDs); err != nil { return err } } return nil }