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>
271 lines
6.7 KiB
Go
271 lines
6.7 KiB
Go
package flow
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
)
|
|
|
|
type Color string
|
|
|
|
const (
|
|
ColorDefault Color = "default"
|
|
ColorPrimary Color = "primary"
|
|
ColorSuccess Color = "success"
|
|
ColorGood Color = "good"
|
|
ColorWarning Color = "warning"
|
|
ColorDanger Color = "danger"
|
|
)
|
|
|
|
type Step struct {
|
|
name Name
|
|
template *model.SlackAttachment
|
|
forwardTo Name
|
|
autoForward bool
|
|
terminal bool
|
|
onRender func(f *Flow)
|
|
buttons []Button
|
|
}
|
|
|
|
type Button struct {
|
|
Name string
|
|
Disabled bool
|
|
Color Color
|
|
|
|
// OnClick is called when the button is clicked. It returns the next step's
|
|
// name and the state updates to apply.
|
|
//
|
|
// If Dialog is also specified, OnClick is executed first.
|
|
OnClick func(f *Flow) (Name, State, error)
|
|
|
|
// Dialog is the interactive dialog to display if the button is clicked
|
|
// (OnClick is executed first). OnDialogSubmit must be provided.
|
|
Dialog *model.Dialog
|
|
|
|
// Function that is called when the dialog box is submitted. It can return a
|
|
// general error, or field-specific errors. On success it returns the name
|
|
// of the next step, and the state updates to apply.
|
|
OnDialogSubmit func(f *Flow, submitted map[string]any) (Name, State, map[string]string, error)
|
|
}
|
|
|
|
func NewStep(name Name) Step {
|
|
return Step{
|
|
name: name,
|
|
template: &model.SlackAttachment{},
|
|
}
|
|
}
|
|
|
|
func (s Step) WithButton(buttons ...Button) Step {
|
|
s.buttons = append(s.buttons, buttons...)
|
|
return s
|
|
}
|
|
|
|
func (s Step) Terminal() Step {
|
|
s.terminal = true
|
|
return s
|
|
}
|
|
|
|
func (s Step) OnRender(f func(*Flow)) Step {
|
|
s.onRender = f
|
|
return s
|
|
}
|
|
|
|
func (s Step) Next(name Name) Step {
|
|
s.forwardTo = name
|
|
s.autoForward = true
|
|
return s
|
|
}
|
|
|
|
func (s Step) WithImage(imageURL string) Step {
|
|
if u, err := url.Parse(imageURL); err == nil {
|
|
if u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") {
|
|
s.template.ImageURL = imageURL
|
|
} else {
|
|
s.template.ImageURL = u.Path
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s Step) WithColor(color Color) Step {
|
|
s.template.Color = string(color)
|
|
return s
|
|
}
|
|
|
|
func (s Step) WithPretext(text string) Step {
|
|
s.template.Pretext = text
|
|
return s
|
|
}
|
|
|
|
func (s Step) WithField(title, value string) Step {
|
|
s.template.Fields = append(s.template.Fields, &model.SlackAttachmentField{
|
|
Title: title,
|
|
Value: value,
|
|
})
|
|
return s
|
|
}
|
|
|
|
func (s Step) WithTitle(text string) Step {
|
|
s.template.Title = text
|
|
return s
|
|
}
|
|
|
|
func (s Step) WithText(text string) Step {
|
|
s.template.Text = text
|
|
return s
|
|
}
|
|
|
|
func (s Step) do(f *Flow) (*model.Post, bool, error) {
|
|
if s.onRender != nil {
|
|
s.onRender(f)
|
|
}
|
|
|
|
return s.render(f, false, 0)
|
|
}
|
|
|
|
func (s Step) done(f *Flow, selectedButton int) (*model.Post, error) {
|
|
post, _, err := s.render(f, true, selectedButton)
|
|
return post, err
|
|
}
|
|
|
|
func (s Step) render(f *Flow, done bool, selectedButton int) (*model.Post, bool, error) {
|
|
sa := f.processAttachment(s.template)
|
|
post := model.Post{}
|
|
model.ParseSlackAttachment(&post, []*model.SlackAttachment{sa})
|
|
|
|
if s.terminal {
|
|
// Nothing else to do, do not display buttons on terminal posts.
|
|
return &post, true, nil
|
|
}
|
|
|
|
buttons := processButtons(s.buttons, f.state.AppState)
|
|
|
|
attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment)
|
|
if !ok || len(attachments) != 1 {
|
|
return nil, false, errors.New("expected 1 slack attachment")
|
|
}
|
|
var actions []*model.PostAction
|
|
if done {
|
|
if selectedButton > 0 {
|
|
action := renderButton(buttons[selectedButton-1], s.name, selectedButton, f.state.AppState)
|
|
action.Disabled = true
|
|
actions = append(actions, action)
|
|
}
|
|
} else {
|
|
for i, b := range buttons {
|
|
actions = append(actions, renderButton(b, s.name, i+1, f.state.AppState))
|
|
}
|
|
}
|
|
attachments[0].Actions = actions
|
|
return &post, false, nil
|
|
}
|
|
|
|
func (f *Flow) processAttachment(attachment *model.SlackAttachment) *model.SlackAttachment {
|
|
if attachment == nil {
|
|
return &model.SlackAttachment{Text: "ERROR"}
|
|
}
|
|
a := *attachment
|
|
a.Pretext = formatState(attachment.Pretext, f.state.AppState)
|
|
a.Title = formatState(attachment.Title, f.state.AppState)
|
|
a.Text = formatState(attachment.Text, f.state.AppState)
|
|
|
|
for _, field := range a.Fields {
|
|
field.Title = formatState(field.Title, f.state.AppState)
|
|
v := field.Value.(string)
|
|
if v != "" {
|
|
field.Value = formatState(v, f.state.AppState)
|
|
}
|
|
}
|
|
|
|
a.Fallback = fmt.Sprintf("%s: %s", a.Title, a.Text)
|
|
|
|
if attachment.ImageURL != "" {
|
|
if u, err := url.Parse(attachment.ImageURL); err == nil {
|
|
if u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") {
|
|
a.ImageURL = attachment.ImageURL
|
|
} else {
|
|
a.ImageURL = fmt.Sprintf("%s/plugins/%s/%s", f.siteURL, f.pluginID, strings.TrimPrefix(attachment.ImageURL, "/"))
|
|
}
|
|
}
|
|
}
|
|
|
|
return &a
|
|
}
|
|
|
|
func processButtons(in []Button, state State) []Button {
|
|
var out []Button
|
|
for _, b := range in {
|
|
button := b
|
|
button.Name = formatState(b.Name, state)
|
|
out = append(out, button)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func processDialog(in *model.Dialog, state State) model.Dialog {
|
|
d := *in
|
|
d.Title = formatState(d.Title, state)
|
|
d.IntroductionText = formatState(d.IntroductionText, state)
|
|
d.SubmitLabel = formatState(d.SubmitLabel, state)
|
|
for i := range d.Elements {
|
|
d.Elements[i].DisplayName = formatState(d.Elements[i].DisplayName, state)
|
|
d.Elements[i].Name = formatState(d.Elements[i].Name, state)
|
|
d.Elements[i].Default = formatState(d.Elements[i].Default, state)
|
|
d.Elements[i].Placeholder = formatState(d.Elements[i].Placeholder, state)
|
|
d.Elements[i].HelpText = formatState(d.Elements[i].HelpText, state)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func renderButton(b Button, stepName Name, i int, state State) *model.PostAction {
|
|
return &model.PostAction{
|
|
Type: model.PostActionTypeButton,
|
|
Name: formatState(b.Name, state),
|
|
Disabled: b.Disabled,
|
|
Style: string(b.Color),
|
|
Integration: &model.PostActionIntegration{
|
|
Context: map[string]any{
|
|
contextStepKey: string(stepName),
|
|
contextButtonKey: strconv.Itoa(i),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func buttonContext(request *model.PostActionIntegrationRequest) (Name, int, error) {
|
|
fromString, ok := request.Context[contextStepKey].(string)
|
|
if !ok {
|
|
return "", 0, errors.New("missing step name")
|
|
}
|
|
fromName := Name(fromString)
|
|
|
|
buttonStr, ok := request.Context[contextButtonKey].(string)
|
|
if !ok {
|
|
return "", 0, errors.New("missing button id")
|
|
}
|
|
buttonIndex, err := strconv.Atoi(buttonStr)
|
|
if err != nil {
|
|
return "", 0, errors.Wrap(err, "invalid button number")
|
|
}
|
|
|
|
return fromName, buttonIndex, nil
|
|
}
|
|
|
|
func dialogContext(request *model.SubmitDialogRequest) (Name, int, error) {
|
|
data := strings.Split(request.State, ",")
|
|
if len(data) != 2 {
|
|
return "", 0, errors.New("invalid request")
|
|
}
|
|
fromName := Name(data[0])
|
|
buttonIndex, err := strconv.Atoi(data[1])
|
|
if err != nil {
|
|
return "", 0, errors.Wrap(err, "malformed button number")
|
|
}
|
|
return fromName, buttonIndex, nil
|
|
}
|