mattermost-community-enterp.../public/pluginapi/experimental/flow/flow.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
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>
2025-12-17 23:59:07 +09:00

271 lines
5.3 KiB
Go

package flow
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/pluginapi"
)
type Name string
const (
contextStepKey = "step"
contextButtonKey = "button"
)
type Flow struct {
UserID string
state *flowState
name Name
api *pluginapi.Client
pluginID string
botUserID string
siteURL string
steps map[Name]Step
index []Name
done func(userID string, state State) error
debugLogState bool
}
// NewFlow creates a new flow using direct messages with the user.
//
// name must be a unique identifier for the flow within the plugin.
func NewFlow(name Name, api *pluginapi.Client, pluginID, botUserID string) (*Flow, error) {
if api == nil {
return nil, errors.New("API client must not be nil")
}
config := api.Configuration.GetConfig()
if config == nil {
return nil, errors.New("failed to fetch configuration")
}
if config.ServiceSettings.SiteURL == nil {
return nil, errors.New("please configure the Mattermost Server's SiteURL, then restart the plugin.")
}
return &Flow{
name: name,
api: api,
pluginID: pluginID,
botUserID: botUserID,
siteURL: strings.TrimRight(*config.ServiceSettings.SiteURL, "/"),
steps: map[Name]Step{},
}, nil
}
func (f *Flow) WithSteps(orderedSteps ...Step) *Flow {
if f.steps == nil {
f.steps = map[Name]Step{}
}
for _, step := range orderedSteps {
stepName := step.name
if _, ok := f.steps[stepName]; ok {
f.api.Log.Warn("ignored duplicate step name", "name", stepName, "flow", f.name)
continue
}
f.steps[stepName] = step
f.index = append(f.index, stepName)
}
return f
}
func (f *Flow) OnDone(done func(string, State) error) *Flow {
f.done = done
return f
}
func (f *Flow) InitHTTP(r *mux.Router) *Flow {
flowRouter := r.PathPrefix("/").Subrouter()
flowRouter.HandleFunc(namePath(f.name)+"/button", f.handleButtonHTTP).Methods(http.MethodPost)
flowRouter.HandleFunc(namePath(f.name)+"/dialog", f.handleDialogHTTP).Methods(http.MethodPost)
return f
}
func (f *Flow) WithDebugLog() *Flow {
f.debugLogState = true
return f
}
// ForUser creates a new flow using direct messages with the user.
func (f *Flow) ForUser(userID string) *Flow {
clone := *f
clone.UserID = userID
clone.state = nil
return &clone
}
func (f *Flow) GetCurrentStep() (Name, error) {
state, err := f.getState()
if err != nil {
// Don't return an error if no flow is running
if errors.Is(err, errStateNotFound) {
return "", nil
}
return "", err
}
return state.StepName, err
}
func (f *Flow) GetState() State {
state, _ := f.getState()
return state.AppState
}
func (f *Flow) Start(appState State) error {
if len(f.index) == 0 {
return errors.New("no steps")
}
err := f.storeState(flowState{
AppState: appState,
})
if err != nil {
return err
}
return f.Go(f.index[0])
}
func (f *Flow) Finish() error {
state, err := f.getState()
if err != nil {
return err
}
_ = f.removeState()
if f.done != nil {
err = f.done(f.UserID, state.AppState)
}
return err
}
func (f *Flow) Go(toName Name) error {
state, err := f.getState()
if err != nil {
return err
}
if toName == state.StepName {
// Stay at the current step, nothing to do
return nil
}
// Moving onto a different step, mark the current step as "Done"
if state.StepName != "" && !state.Done {
from, ok := f.steps[state.StepName]
if !ok {
return errors.Errorf("%s: step not found", toName)
}
var donePost *model.Post
donePost, err = from.done(f, 0)
if err != nil {
return err
}
if donePost != nil {
donePost.Id = state.PostID
err = f.api.Post.UpdatePost(donePost)
if err != nil {
return err
}
}
}
if toName == "" {
return f.Finish()
}
to, ok := f.steps[toName]
if !ok {
return errors.Errorf("%s: step not found", toName)
}
post, terminal, err := to.do(f)
if err != nil {
return err
}
f.processButtonPostActions(post)
if f.debugLogState {
data, _ := json.MarshalIndent(state, "", " ")
post.Message = fmt.Sprintf("State:\n```\n%s\n```\n", string(data))
}
err = f.api.Post.DM(f.botUserID, f.UserID, post)
if err != nil {
return err
}
if terminal {
return f.Finish()
}
state.StepName = toName
state.Done = false
state.PostID = post.Id
err = f.storeState(state)
if err != nil {
return err
}
if to.autoForward {
var nextName Name
if to.forwardTo != "" {
nextName = to.forwardTo
} else {
nextName = f.next(toName)
}
if nextName != "" {
return f.Go(nextName)
}
}
return nil
}
func (f Flow) next(fromName Name) Name {
for i, n := range f.index {
if fromName == n {
if i+1 < len(f.index) {
return f.index[i+1]
}
return ""
}
}
return ""
}
func namePath(name Name) string {
return "/" + url.PathEscape(strings.Trim(string(name), "/"))
}
func Goto(toName Name) func(*Flow) (Name, State, error) {
return func(_ *Flow) (Name, State, error) {
return toName, nil, nil
}
}
func DialogGoto(toName Name) func(*Flow, map[string]any) (Name, State, map[string]string, error) {
return func(_ *Flow, submitted map[string]any) (Name, State, map[string]string, error) {
stateUpdate := State{}
for k, v := range submitted {
stateUpdate[k] = fmt.Sprintf("%v", v)
}
return toName, stateUpdate, nil, nil
}
}