Files
mostlymatter/app/work_template_executor.go
2023-01-25 10:24:01 -07:00

279 строки
9.1 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
fb_model "github.com/mattermost/focalboard/server/model"
pbclient "github.com/mattermost/mattermost-plugin-playbooks/client"
"github.com/mattermost/mattermost-server/v6/app/request"
"github.com/mattermost/mattermost-server/v6/app/worktemplates"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/product"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/store"
)
type WorkTemplateExecutor interface {
CreatePlaybook(c *request.Context, wtcr *worktemplates.ExecutionRequest, playbook *model.WorkTemplatePlaybook, channel model.WorkTemplateChannel) (string, error)
CreateChannel(c *request.Context, wtcr *worktemplates.ExecutionRequest, cChannel *model.WorkTemplateChannel) (string, error)
CreateBoard(c *request.Context, wtcr *worktemplates.ExecutionRequest, cBoard *model.WorkTemplateBoard, linkToChannelID string) (string, error)
InstallPlugin(c *request.Context, wtcr *worktemplates.ExecutionRequest, cIntegration *model.WorkTemplateIntegration, sendToChannelID string) error
}
type appWorkTemplateExecutor struct {
app *App
}
func (e *appWorkTemplateExecutor) CreatePlaybook(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
playbook *model.WorkTemplatePlaybook,
channel model.WorkTemplateChannel) (string, error) {
// determine playbook name
name := playbook.Name
if wtcr.Name != "" {
name = fmt.Sprintf("%s: %s", wtcr.Name, playbook.Name)
}
// get the correct playbook pbTemplate
pbTemplate, err := wtcr.FindPlaybookTemplate(playbook.Template)
if err != nil {
return "", fmt.Errorf("unable to find playbook template: %w", err)
}
pbTemplate.TeamID = wtcr.TeamID
pbTemplate.Title = name
pbTemplate.Public = wtcr.Visibility == model.WorkTemplateVisibilityPublic
data, err := json.Marshal(pbTemplate)
if err != nil {
return "", fmt.Errorf("unable to marshal playbook template: %w", err)
}
resp, appErr := e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/playbooks", nil, data)
if appErr != nil {
return "", fmt.Errorf("unable to create playbook: %w", appErr)
}
defer resp.Body.Close()
pbcResp := playbookCreateResponse{}
err = json.NewDecoder(resp.Body).Decode(&pbcResp)
if err != nil {
return "", fmt.Errorf("unable to decode playbook create response: %w", err)
}
runName := channel.Name
if wtcr.Name != "" {
runName = fmt.Sprintf("%s: %s", wtcr.Name, channel.Name)
}
data, err = json.Marshal(pbclient.PlaybookRunCreateOptions{
Name: runName,
OwnerUserID: c.Session().UserId,
TeamID: wtcr.TeamID,
PlaybookID: pbcResp.ID,
})
if err != nil {
return "", fmt.Errorf("unable to marshal playbook run create request: %w", err)
}
resp, appErr = e.app.doPluginRequest(c, http.MethodPost, "/plugins/playbooks/api/v0/runs", nil, data)
if appErr != nil {
return "", fmt.Errorf("unable to create playbook run: %w", appErr)
}
defer resp.Body.Close()
pbrResp := playbookRunCreateResponse{}
err = json.NewDecoder(resp.Body).Decode(&pbrResp)
if err != nil {
return "", fmt.Errorf("unable to decode playbook run create response: %w", err)
}
// using pbrResp.ChannelID, update the channel to add metadata
dbChannel, err := e.app.Srv().Store().Channel().Get(pbrResp.ChannelID, false)
if err != nil {
return "", fmt.Errorf("unable to find channel: %w", err)
}
if dbChannel == nil {
return "", fmt.Errorf("channel not found")
}
dbChannel.AddProp(model.WorkTemplateIDChannelProp, wtcr.WorkTemplate.ID)
_, err = e.app.Srv().Store().Channel().Update(dbChannel)
if err != nil {
e.app.Srv().Log().Error("Failed to update playbook channel metadata", mlog.Err(err))
}
return pbrResp.ChannelID, nil
}
func (e *appWorkTemplateExecutor) CreateChannel(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cChannel *model.WorkTemplateChannel,
) (string, error) {
channelID := ""
channelDisplayName := cChannel.Name
if wtcr.Name != "" {
channelDisplayName = fmt.Sprintf("%s: %s", wtcr.Name, cChannel.Name)
}
var channelCreationAppErr *model.AppError = &model.AppError{}
cleanChannelName := cleanChannelName(channelDisplayName)
channelName := cleanChannelName
if len(channelName) > model.ChannelNameMaxLength {
channelName = channelName[:model.ChannelNameMaxLength]
}
// Mostly because of the "quick use" feature, we might try to create channel that have the exact same "Name"
// This loop ensures that if the original name is taken, we try again by adding a suffix to the Name
for channelCreationAppErr != nil {
// create channel
var newChan *model.Channel
newChan, channelCreationAppErr = e.app.CreateChannelWithUser(c, &model.Channel{
TeamId: wtcr.TeamID,
Name: channelName,
DisplayName: channelDisplayName,
Type: model.ChannelTypeOpen,
Purpose: cChannel.Purpose,
Props: map[string]any{
model.WorkTemplateIDChannelProp: wtcr.WorkTemplate.ID,
},
}, c.Session().UserId)
if channelCreationAppErr != nil {
if channelCreationAppErr.Id == store.ChannelExistsError {
// compute a new unique name
suffix := fmt.Sprintf("-%s", model.NewId()[0:4])
channelName = cleanChannelName
if len(cleanChannelName)+len(suffix) > model.ChannelNameMaxLength {
channelName = cleanChannelName[:model.ChannelNameMaxLength-len(suffix)]
}
channelName = channelName + suffix
continue
}
return "", fmt.Errorf("error while creating channel: %w", channelCreationAppErr)
}
channelID = newChan.Id
}
return channelID, nil
}
func (e *appWorkTemplateExecutor) CreateBoard(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cBoard *model.WorkTemplateBoard,
linkToChannelID string,
) (string, error) {
boardService := e.app.Srv().services[product.BoardsKey].(product.BoardsService)
templates, err := boardService.GetTemplates("0", c.Session().UserId)
if err != nil {
return "", fmt.Errorf("error while getting templates: %w", err)
}
var template *fb_model.Board = nil
for _, t := range templates {
v, ok := t.Properties["trackingTemplateId"]
if ok && v == cBoard.Template {
template = t
break
}
}
if template == nil {
return "", fmt.Errorf("template not found")
}
title := cBoard.Name
if wtcr.Name != "" {
title = fmt.Sprintf("%s: %s", wtcr.Name, cBoard.Name)
}
// Duplicate board From template
boardsAndBlocks, _, err := boardService.DuplicateBoard(template.ID, c.Session().UserId, wtcr.TeamID, false)
if err != nil {
return "", fmt.Errorf("failed to create new board from template: %w", err)
}
if len(boardsAndBlocks.Boards) != 1 {
return "", fmt.Errorf("only one board was expected, found %d", len(boardsAndBlocks.Boards))
}
// Apply patch for the title and linked channel
patchedBoard, err := boardService.PatchBoard(&fb_model.BoardPatch{
Title: &title,
ChannelID: &linkToChannelID,
}, boardsAndBlocks.Boards[0].ID, c.Session().UserId)
if err != nil {
return "", fmt.Errorf("failed to patch board: %w", err)
}
return patchedBoard.ID, nil
}
func (e *appWorkTemplateExecutor) InstallPlugin(
c *request.Context,
wtcr *worktemplates.ExecutionRequest,
cIntegration *model.WorkTemplateIntegration,
sendToChannelID string,
) error {
// check if this plugin is already installed
pluginID := cIntegration.ID
_, appErr := e.app.GetPluginStatus(pluginID)
if appErr != nil {
if appErr.Id == "app.plugin.not_installed.app_error" {
// we install them in the background as we don't want user to wait for this
manifest, installAppErr := e.app.Channels().InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{
Id: pluginID,
Version: "",
})
if installAppErr != nil {
return fmt.Errorf("unable to install plugin: %w", installAppErr)
}
if sendToChannelID != "" {
e.app.SendEphemeralPost(c, c.Session().UserId, &model.Post{
ChannelId: sendToChannelID,
Message: fmt.Sprintf("plugin %s has been installed", manifest.Name),
CreateAt: model.GetMillis(),
})
}
} else {
return fmt.Errorf("unable to get plugin status: %w", appErr)
}
}
// get plugin state
if err := e.app.EnablePlugin(pluginID); err != nil {
return fmt.Errorf("unable to enable plugin: %w", err)
}
return nil
}
type playbookCreateResponse struct {
ID string `json:"id"`
}
type playbookRunCreateResponse struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
}
// cleaning channel name code bellow comes from the playbook repository.
var allNonSpaceNonWordRegex = regexp.MustCompile(`[^\w\s]`)
func cleanChannelName(channelName string) string {
// Lower case only
channelName = strings.ToLower(channelName)
// Trim spaces
channelName = strings.TrimSpace(channelName)
// Change all dashes to whitespace, remove everything that's not a word or whitespace, all space becomes dashes
channelName = strings.ReplaceAll(channelName, "-", " ")
channelName = allNonSpaceNonWordRegex.ReplaceAllString(channelName, "")
channelName = strings.ReplaceAll(channelName, " ", "-")
// Remove all leading and trailing dashes
channelName = strings.Trim(channelName, "-")
return channelName
}