[MM-53968] Includes mattermost-plugin-api into the mono repo (#24235)
Include https://github.com/mattermost/mattermost-plugin-api into the mono repo Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Michael Kochell <mjkochell@gmail.com> Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com> Co-authored-by: Alex Dovenmuehle <alex.dovenmuehle@mattermost.com> Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> Co-authored-by: Christopher Poile <cpoile@gmail.com> Co-authored-by: İlker Göktuğ Öztürk <ilkergoktugozturk@gmail.com> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com> Co-authored-by: Daniel Espino García <larkox@gmail.com> Co-authored-by: Christopher Speller <crspeller@gmail.com> Co-authored-by: Alex Dovenmuehle <adovenmuehle@gmail.com> Co-authored-by: Szymon Gibała <szymongib@gmail.com> Co-authored-by: Lev <1187448+levb@users.noreply.github.com> Co-authored-by: Jason Frerich <jason.frerich@mattermost.com> Co-authored-by: Agniva De Sarker <agnivade@yahoo.co.in> Co-authored-by: Artur M. Wolff <artur.m.wolff@gmail.com> Co-authored-by: Madhav Hugar <16546715+madhavhugar@users.noreply.github.com> Co-authored-by: Joe <security.joe@pm.me> Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> Co-authored-by: José Peso <trilopin@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
bc11b29807
Коммит
3ee5432664
254
server/public/pluginapi/experimental/flow/flow.go
Обычный файл
254
server/public/pluginapi/experimental/flow/flow.go
Обычный файл
@@ -0,0 +1,254 @@
|
||||
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
|
||||
pluginURL string
|
||||
botUserID 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, pluginURL, botUserID string) *Flow {
|
||||
return &Flow{
|
||||
name: name,
|
||||
api: api,
|
||||
pluginURL: pluginURL,
|
||||
botUserID: botUserID,
|
||||
steps: map[Name]Step{},
|
||||
}
|
||||
}
|
||||
|
||||
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]interface{}) (Name, State, map[string]string, error) {
|
||||
return func(_ *Flow, submitted map[string]interface{}) (Name, State, map[string]string, error) {
|
||||
stateUpdate := State{}
|
||||
for k, v := range submitted {
|
||||
stateUpdate[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
return toName, stateUpdate, nil, nil
|
||||
}
|
||||
}
|
||||
210
server/public/pluginapi/experimental/flow/handler.go
Обычный файл
210
server/public/pluginapi/experimental/flow/handler.go
Обычный файл
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License for license information.
|
||||
|
||||
package flow
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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]interface{},
|
||||
) (
|
||||
*model.Post, map[string]string, error,
|
||||
) {
|
||||
return f.handle(fromName, selectedButton, submission, "", false)
|
||||
}
|
||||
|
||||
func (f *Flow) handle(
|
||||
fromName Name, selectedButton int, submission map[string]interface{}, 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
|
||||
}
|
||||
state.AppState = state.AppState.MergeWith(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: f.pluginURL + 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("attachments").([]*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 = f.pluginURL + namePath(f.name) + "/button"
|
||||
}
|
||||
}
|
||||
146
server/public/pluginapi/experimental/flow/state.go
Обычный файл
146
server/public/pluginapi/experimental/flow/state.go
Обычный файл
@@ -0,0 +1,146 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
var errStateNotFound = errors.New("flow state not found")
|
||||
|
||||
// State is the "app"'s state
|
||||
type State map[string]interface{}
|
||||
|
||||
func (s State) MergeWith(update State) State {
|
||||
n := State{}
|
||||
for k, v := range s {
|
||||
n[k] = v
|
||||
}
|
||||
for k, v := range update {
|
||||
n[k] = v
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// GetString return the value to a given key as a string.
|
||||
// If the key is not found or isn't a string, an empty string is returned.
|
||||
func (s State) GetString(key string) string {
|
||||
vRaw, ok := s[key]
|
||||
if ok {
|
||||
v, ok := vRaw.(string)
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetInt return the value to a given key as a int.
|
||||
// If the key is not found or isn't an int, zero is returned.
|
||||
func (s State) GetInt(key string) int {
|
||||
vRaw, ok := s[key]
|
||||
if ok {
|
||||
v, ok := vRaw.(int)
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetBool return the value to a given key as a bool.
|
||||
// If the key is not found or isn't a bool, false is returned.
|
||||
func (s State) GetBool(key string) bool {
|
||||
vRaw, ok := s[key]
|
||||
if ok {
|
||||
v, ok := vRaw.(bool)
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// JSON-serializable flow state.
|
||||
type flowState struct {
|
||||
// The name of the step.
|
||||
StepName Name
|
||||
|
||||
Done bool
|
||||
|
||||
// ID of the post produced by the step.
|
||||
PostID string
|
||||
|
||||
// Application-level state.
|
||||
AppState State
|
||||
}
|
||||
|
||||
func (f *Flow) storeState(state flowState) error {
|
||||
if f.UserID == "" {
|
||||
return errors.New("no user specified")
|
||||
}
|
||||
|
||||
// Set AppState to differentiate an existing flow
|
||||
if state.AppState == nil {
|
||||
state.AppState = State{}
|
||||
}
|
||||
|
||||
ok, err := f.api.KV.Set(kvKey(f.UserID, f.name), state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errors.New("value not set without errors")
|
||||
}
|
||||
|
||||
f.state = &state
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Flow) getState() (flowState, error) {
|
||||
if f.UserID == "" {
|
||||
return flowState{}, errors.New("no user specified")
|
||||
}
|
||||
if f.state != nil {
|
||||
return *f.state, nil
|
||||
}
|
||||
state := flowState{}
|
||||
err := f.api.KV.Get(kvKey(f.UserID, f.name), &state)
|
||||
if err != nil {
|
||||
return flowState{}, err
|
||||
}
|
||||
if state.AppState == nil {
|
||||
return flowState{}, errStateNotFound
|
||||
}
|
||||
|
||||
f.state = &state
|
||||
return state, err
|
||||
}
|
||||
|
||||
func (f *Flow) removeState() error {
|
||||
if f.UserID == "" {
|
||||
return errors.New("no user specified")
|
||||
}
|
||||
f.state = nil
|
||||
return f.api.KV.Delete(kvKey(f.UserID, f.name))
|
||||
}
|
||||
|
||||
func kvKey(userID string, flowName Name) string {
|
||||
return "_flow-" + userID + "-" + string(flowName)
|
||||
}
|
||||
|
||||
func formatState(source string, state State) string {
|
||||
t, err := template.New("message").Parse(source)
|
||||
if err != nil {
|
||||
return source + " ###ERROR: " + err.Error()
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err = t.Execute(buf, state)
|
||||
if err != nil {
|
||||
return source + " ###ERROR: " + err.Error()
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
269
server/public/pluginapi/experimental/flow/step.go
Обычный файл
269
server/public/pluginapi/experimental/flow/step.go
Обычный файл
@@ -0,0 +1,269 @@
|
||||
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]interface{}) (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("attachments").([]*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 = f.pluginURL + "/" + 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{
|
||||
Name: formatState(b.Name, state),
|
||||
Disabled: b.Disabled,
|
||||
Style: string(b.Color),
|
||||
Integration: &model.PostActionIntegration{
|
||||
Context: map[string]interface{}{
|
||||
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
|
||||
}
|
||||
Ссылка в новой задаче
Block a user