Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
212 lines
5.5 KiB
Go
212 lines
5.5 KiB
Go
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
|
|
// See License for license information.
|
|
|
|
package flow
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/common"
|
|
)
|
|
|
|
func (f *Flow) handleButtonHTTP(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("Mattermost-User-ID")
|
|
if userID == "" {
|
|
common.SlackAttachmentError(w, errors.New("Not authorized"))
|
|
return
|
|
}
|
|
f = f.ForUser(userID)
|
|
|
|
var request model.PostActionIntegrationRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
common.SlackAttachmentError(w, errors.New("invalid request"))
|
|
return
|
|
}
|
|
|
|
// selectedButton is 1-based
|
|
fromName, selectedButton, err := buttonContext(&request)
|
|
if err != nil {
|
|
common.SlackAttachmentError(w, err)
|
|
return
|
|
}
|
|
|
|
donePost, err := f.handleButton(fromName, selectedButton, request.TriggerId)
|
|
if err != nil {
|
|
common.SlackAttachmentError(w, err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(model.PostActionIntegrationResponse{
|
|
Update: donePost,
|
|
})
|
|
}
|
|
|
|
func (f *Flow) handleDialogHTTP(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("Mattermost-User-ID")
|
|
if userID == "" {
|
|
common.DialogError(w, errors.New("not authorized"))
|
|
return
|
|
}
|
|
f = f.ForUser(userID)
|
|
|
|
var request model.SubmitDialogRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
common.DialogError(w, errors.New("invalid request"))
|
|
return
|
|
}
|
|
fromName, selectedButton, err := dialogContext(&request)
|
|
if err != nil {
|
|
common.DialogError(w, errors.Wrap(err, "invalid request"))
|
|
return
|
|
}
|
|
|
|
// handleDialog updates the post
|
|
donePost, fieldErrors, err := f.handleDialog(fromName, selectedButton, request.Submission)
|
|
if err != nil || len(fieldErrors) != 0 {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
resp := model.SubmitDialogResponse{
|
|
Errors: fieldErrors,
|
|
}
|
|
|
|
if err != nil {
|
|
resp.Error = err.Error()
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
return
|
|
}
|
|
err = f.api.Post.UpdatePost(donePost)
|
|
if err != nil {
|
|
common.DialogError(w, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(model.SubmitDialogResponse{})
|
|
}
|
|
|
|
func (f *Flow) handleButton(fromName Name, selectedButton int, triggerID string) (*model.Post, error) {
|
|
post, _, err := f.handle(fromName, selectedButton, nil, triggerID, true)
|
|
return post, err
|
|
}
|
|
|
|
func (f *Flow) handleDialog(
|
|
fromName Name, selectedButton int, submission map[string]any,
|
|
) (
|
|
*model.Post, map[string]string, error,
|
|
) {
|
|
return f.handle(fromName, selectedButton, submission, "", false)
|
|
}
|
|
|
|
func (f *Flow) handle(
|
|
fromName Name, selectedButton int, submission map[string]any, triggerID string, asButton bool,
|
|
) (
|
|
*model.Post, map[string]string, error,
|
|
) {
|
|
state, err := f.getState()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if state.StepName != fromName {
|
|
return nil, nil, errors.Errorf("click from an inactive step: %v", fromName)
|
|
}
|
|
from, ok := f.steps[fromName]
|
|
if !ok {
|
|
return nil, nil, errors.Errorf("step %q not found", fromName)
|
|
}
|
|
|
|
if selectedButton == 0 || selectedButton > len(from.buttons) {
|
|
return nil, nil, errors.Errorf("button number %v to high or too low, only %v buttons", selectedButton, len(from.buttons))
|
|
}
|
|
b := from.buttons[selectedButton-1]
|
|
|
|
var updated State
|
|
toName := fromName
|
|
var fieldErrors map[string]string
|
|
if asButton {
|
|
if b.OnClick != nil {
|
|
toName, updated, err = b.OnClick(f)
|
|
}
|
|
} else {
|
|
if b.OnDialogSubmit != nil {
|
|
toName, updated, fieldErrors, err = b.OnDialogSubmit(f, submission)
|
|
}
|
|
}
|
|
if err != nil || len(fieldErrors) > 0 {
|
|
return nil, fieldErrors, err
|
|
}
|
|
maps.Copy(state.AppState, updated)
|
|
state.Done = true
|
|
err = f.storeState(state)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Empty next step name in the response indicates advancing to the next step
|
|
// in the flow. To stay on the same step the handlers should return the step
|
|
// name.
|
|
if toName == "" {
|
|
toName = f.next(fromName)
|
|
}
|
|
|
|
if asButton && b.Dialog != nil {
|
|
if b.OnDialogSubmit == nil {
|
|
return nil, nil, errors.Errorf("no submit function for dialog, step: %s", fromName)
|
|
}
|
|
|
|
dialogRequest := model.OpenDialogRequest{
|
|
TriggerId: triggerID,
|
|
URL: "/plugins/" + f.pluginID + namePath(f.name) + "/dialog",
|
|
Dialog: processDialog(b.Dialog, state.AppState),
|
|
}
|
|
dialogRequest.Dialog.State = fmt.Sprintf("%v,%v", fromName, selectedButton)
|
|
|
|
err = f.api.Frontend.OpenInteractiveDialog(dialogRequest)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
if toName == fromName {
|
|
// Nothing else to do
|
|
return nil, nil, nil
|
|
}
|
|
|
|
donePost, err := from.done(f, selectedButton)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
donePost.Id = state.PostID
|
|
f.processButtonPostActions(donePost)
|
|
|
|
err = f.Go(toName)
|
|
if err != nil {
|
|
f.api.Log.Warn("failed to advance flow to next step", "flow_name", f.name, "from", fromName, "to", toName, "error", err.Error())
|
|
}
|
|
|
|
// return the "done" post for the from step - leave updating up to the
|
|
// API-specific caller.
|
|
return donePost, nil, nil
|
|
}
|
|
|
|
func (f *Flow) processButtonPostActions(post *model.Post) {
|
|
attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment)
|
|
if !ok || len(attachments) == 0 {
|
|
return
|
|
}
|
|
sa := attachments[0]
|
|
for _, a := range sa.Actions {
|
|
if a.Integration == nil {
|
|
a.Integration = &model.PostActionIntegration{}
|
|
}
|
|
a.Integration.URL = "/plugins/" + f.pluginID + namePath(f.name) + "/button"
|
|
}
|
|
}
|