Files
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05: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
}
}