[MM-47846] Execute Work Template (#22055)
Этот коммит содержится в:
278
app/work_template_executor.go
Обычный файл
278
app/work_template_executor.go
Обычный файл
@@ -0,0 +1,278 @@
|
||||
// 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
|
||||
}
|
||||
Ссылка в новой задаче
Block a user