github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/app/integration_action.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  // Integration Action Flow
     5  //
     6  // 1. An integration creates an interactive message button or menu.
     7  // 2. A user clicks on a button or selects an option from the menu.
     8  // 3. The client sends a request to server to complete the post action, calling DoPostAction below.
     9  // 4. DoPostAction will send an HTTP POST request to the integration containing contextual data, including
    10  // an encoded and signed trigger ID. Slash commands also include trigger IDs in their payloads.
    11  // 5. The integration performs any actions it needs to and optionally makes a request back to the MM server
    12  // using the trigger ID to open an interactive dialog.
    13  // 6. If that optional request is made, OpenInteractiveDialog sends a WebSocket event to all connected clients
    14  // for the relevant user, telling them to display the dialog.
    15  // 7. The user fills in the dialog and submits it, where SubmitInteractiveDialog will submit it back to the
    16  // integration for handling.
    17  
    18  package app
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/url"
    29  	"path"
    30  	"path/filepath"
    31  	"strings"
    32  
    33  	"github.com/gorilla/mux"
    34  
    35  	"github.com/mattermost/mattermost-server/v5/mlog"
    36  	"github.com/mattermost/mattermost-server/v5/model"
    37  	"github.com/mattermost/mattermost-server/v5/store"
    38  	"github.com/mattermost/mattermost-server/v5/utils"
    39  )
    40  
    41  func (a *App) DoPostAction(postId, actionId, userID, selectedOption string) (string, *model.AppError) {
    42  	return a.DoPostActionWithCookie(postId, actionId, userID, selectedOption, nil)
    43  }
    44  
    45  func (a *App) DoPostActionWithCookie(postId, actionId, userID, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
    46  
    47  	// PostAction may result in the original post being updated. For the
    48  	// updated post, we need to unconditionally preserve the original
    49  	// IsPinned and HasReaction attributes, and preserve its entire
    50  	// original Props set unless the plugin returns a replacement value.
    51  	// originalXxx variables are used to preserve these values.
    52  	var originalProps map[string]interface{}
    53  	originalIsPinned := false
    54  	originalHasReactions := false
    55  
    56  	// If the updated post does contain a replacement Props set, we still
    57  	// need to preserve some original values, as listed in
    58  	// model.PostActionRetainPropKeys. remove and retain track these.
    59  	remove := []string{}
    60  	retain := map[string]interface{}{}
    61  
    62  	datasource := ""
    63  	upstreamURL := ""
    64  	rootPostId := ""
    65  	upstreamRequest := &model.PostActionIntegrationRequest{
    66  		UserId: userID,
    67  		PostId: postId,
    68  	}
    69  
    70  	// See if the post exists in the DB, if so ignore the cookie.
    71  	// Start all queries here for parallel execution
    72  	pchan := make(chan store.StoreResult, 1)
    73  	go func() {
    74  		post, err := a.Srv().Store.Post().GetSingle(postId)
    75  		pchan <- store.StoreResult{Data: post, NErr: err}
    76  		close(pchan)
    77  	}()
    78  
    79  	cchan := make(chan store.StoreResult, 1)
    80  	go func() {
    81  		channel, err := a.Srv().Store.Channel().GetForPost(postId)
    82  		cchan <- store.StoreResult{Data: channel, NErr: err}
    83  		close(cchan)
    84  	}()
    85  
    86  	userChan := make(chan store.StoreResult, 1)
    87  	go func() {
    88  		user, err := a.Srv().Store.User().Get(context.Background(), upstreamRequest.UserId)
    89  		userChan <- store.StoreResult{Data: user, NErr: err}
    90  		close(userChan)
    91  	}()
    92  
    93  	result := <-pchan
    94  	if result.NErr != nil {
    95  		if cookie == nil {
    96  			var nfErr *store.ErrNotFound
    97  			switch {
    98  			case errors.As(result.NErr, &nfErr):
    99  				return "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, nfErr.Error(), http.StatusNotFound)
   100  			default:
   101  				return "", model.NewAppError("DoPostActionWithCookie", "app.post.get.app_error", nil, result.NErr.Error(), http.StatusInternalServerError)
   102  			}
   103  		}
   104  		if cookie.Integration == nil {
   105  			return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "no Integration in action cookie", http.StatusBadRequest)
   106  		}
   107  
   108  		if postId != cookie.PostId {
   109  			return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "postId doesn't match", http.StatusBadRequest)
   110  		}
   111  
   112  		channel, err := a.Srv().Store.Channel().Get(cookie.ChannelId, true)
   113  		if err != nil {
   114  			var nfErr *store.ErrNotFound
   115  			switch {
   116  			case errors.As(err, &nfErr):
   117  				return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get.existing.app_error", nil, nfErr.Error(), http.StatusNotFound)
   118  			default:
   119  				return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get.find.app_error", nil, err.Error(), http.StatusInternalServerError)
   120  			}
   121  		}
   122  
   123  		upstreamRequest.ChannelId = cookie.ChannelId
   124  		upstreamRequest.ChannelName = channel.Name
   125  		upstreamRequest.TeamId = channel.TeamId
   126  		upstreamRequest.Type = cookie.Type
   127  		upstreamRequest.Context = cookie.Integration.Context
   128  		datasource = cookie.DataSource
   129  
   130  		retain = cookie.RetainProps
   131  		remove = cookie.RemoveProps
   132  		rootPostId = cookie.RootPostId
   133  		upstreamURL = cookie.Integration.URL
   134  	} else {
   135  		post := result.Data.(*model.Post)
   136  		result = <-cchan
   137  		if result.NErr != nil {
   138  			return "", model.NewAppError("DoPostActionWithCookie", "app.channel.get_for_post.app_error", nil, result.NErr.Error(), http.StatusInternalServerError)
   139  		}
   140  		channel := result.Data.(*model.Channel)
   141  
   142  		action := post.GetAction(actionId)
   143  		if action == nil || action.Integration == nil {
   144  			return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_id.app_error", nil, fmt.Sprintf("action=%v", action), http.StatusNotFound)
   145  		}
   146  
   147  		upstreamRequest.ChannelId = post.ChannelId
   148  		upstreamRequest.ChannelName = channel.Name
   149  		upstreamRequest.TeamId = channel.TeamId
   150  		upstreamRequest.Type = action.Type
   151  		upstreamRequest.Context = action.Integration.Context
   152  		datasource = action.DataSource
   153  
   154  		// Save the original values that may need to be preserved (including selected
   155  		// Props, i.e. override_username, override_icon_url)
   156  		for _, key := range model.PostActionRetainPropKeys {
   157  			value, ok := post.GetProps()[key]
   158  			if ok {
   159  				retain[key] = value
   160  			} else {
   161  				remove = append(remove, key)
   162  			}
   163  		}
   164  		originalProps = post.GetProps()
   165  		originalIsPinned = post.IsPinned
   166  		originalHasReactions = post.HasReactions
   167  
   168  		if post.RootId == "" {
   169  			rootPostId = post.Id
   170  		} else {
   171  			rootPostId = post.RootId
   172  		}
   173  
   174  		upstreamURL = action.Integration.URL
   175  	}
   176  
   177  	teamChan := make(chan store.StoreResult, 1)
   178  
   179  	go func() {
   180  		defer close(teamChan)
   181  
   182  		// Direct and group channels won't have teams.
   183  		if upstreamRequest.TeamId == "" {
   184  			return
   185  		}
   186  
   187  		team, err := a.Srv().Store.Team().Get(upstreamRequest.TeamId)
   188  		teamChan <- store.StoreResult{Data: team, NErr: err}
   189  	}()
   190  
   191  	ur := <-userChan
   192  	if ur.NErr != nil {
   193  		var nfErr *store.ErrNotFound
   194  		switch {
   195  		case errors.As(ur.NErr, &nfErr):
   196  			return "", model.NewAppError("DoPostActionWithCookie", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound)
   197  		default:
   198  			return "", model.NewAppError("DoPostActionWithCookie", "app.user.get.app_error", nil, ur.NErr.Error(), http.StatusInternalServerError)
   199  		}
   200  	}
   201  	user := ur.Data.(*model.User)
   202  	upstreamRequest.UserName = user.Username
   203  
   204  	tr, ok := <-teamChan
   205  	if ok {
   206  		if tr.NErr != nil {
   207  			var nfErr *store.ErrNotFound
   208  			switch {
   209  			case errors.As(tr.NErr, &nfErr):
   210  				return "", model.NewAppError("DoPostActionWithCookie", "app.team.get.find.app_error", nil, nfErr.Error(), http.StatusNotFound)
   211  			default:
   212  				return "", model.NewAppError("DoPostActionWithCookie", "app.team.get.finding.app_error", nil, tr.NErr.Error(), http.StatusInternalServerError)
   213  			}
   214  		}
   215  
   216  		team := tr.Data.(*model.Team)
   217  		upstreamRequest.TeamName = team.Name
   218  	}
   219  
   220  	if upstreamRequest.Type == model.POST_ACTION_TYPE_SELECT {
   221  		if selectedOption != "" {
   222  			if upstreamRequest.Context == nil {
   223  				upstreamRequest.Context = map[string]interface{}{}
   224  			}
   225  			upstreamRequest.DataSource = datasource
   226  			upstreamRequest.Context["selected_option"] = selectedOption
   227  		}
   228  	}
   229  
   230  	clientTriggerId, _, appErr := upstreamRequest.GenerateTriggerId(a.AsymmetricSigningKey())
   231  	if appErr != nil {
   232  		return "", appErr
   233  	}
   234  
   235  	var resp *http.Response
   236  	if strings.HasPrefix(upstreamURL, "/warn_metrics/") {
   237  		appErr = a.doLocalWarnMetricsRequest(upstreamURL, upstreamRequest)
   238  		if appErr != nil {
   239  			return "", appErr
   240  		}
   241  		return "", nil
   242  	}
   243  	resp, appErr = a.DoActionRequest(upstreamURL, upstreamRequest.ToJson())
   244  	if appErr != nil {
   245  		return "", appErr
   246  	}
   247  	defer resp.Body.Close()
   248  
   249  	var response model.PostActionIntegrationResponse
   250  	if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
   251  		return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
   252  	}
   253  
   254  	if response.Update != nil {
   255  		response.Update.Id = postId
   256  
   257  		// Restore the post attributes and Props that need to be preserved
   258  		if response.Update.GetProps() == nil {
   259  			response.Update.SetProps(originalProps)
   260  		} else {
   261  			for key, value := range retain {
   262  				response.Update.AddProp(key, value)
   263  			}
   264  			for _, key := range remove {
   265  				response.Update.DelProp(key)
   266  			}
   267  		}
   268  		response.Update.IsPinned = originalIsPinned
   269  		response.Update.HasReactions = originalHasReactions
   270  
   271  		if _, appErr = a.UpdatePost(response.Update, false); appErr != nil {
   272  			return "", appErr
   273  		}
   274  	}
   275  
   276  	if response.EphemeralText != "" {
   277  		ephemeralPost := &model.Post{
   278  			Message:   response.EphemeralText,
   279  			ChannelId: upstreamRequest.ChannelId,
   280  			RootId:    rootPostId,
   281  			UserId:    userID,
   282  		}
   283  
   284  		if !response.SkipSlackParsing {
   285  			ephemeralPost.Message = model.ParseSlackLinksToMarkdown(response.EphemeralText)
   286  		}
   287  
   288  		for key, value := range retain {
   289  			ephemeralPost.AddProp(key, value)
   290  		}
   291  		a.SendEphemeralPost(userID, ephemeralPost)
   292  	}
   293  
   294  	return clientTriggerId, nil
   295  }
   296  
   297  // Perform an HTTP POST request to an integration's action endpoint.
   298  // Caller must consume and close returned http.Response as necessary.
   299  // For internal requests, requests are routed directly to a plugin ServerHTTP hook
   300  func (a *App) DoActionRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
   301  	inURL, err := url.Parse(rawURL)
   302  	if err != nil {
   303  		return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, err.Error(), http.StatusBadRequest)
   304  	}
   305  
   306  	rawURLPath := path.Clean(rawURL)
   307  	if strings.HasPrefix(rawURLPath, "/plugins/") || strings.HasPrefix(rawURLPath, "plugins/") {
   308  		return a.DoLocalRequest(rawURLPath, body)
   309  	}
   310  
   311  	req, err := http.NewRequest("POST", rawURL, bytes.NewReader(body))
   312  	if err != nil {
   313  		return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, err.Error(), http.StatusBadRequest)
   314  	}
   315  	req.Header.Set("Content-Type", "application/json")
   316  	req.Header.Set("Accept", "application/json")
   317  
   318  	// Allow access to plugin routes for action buttons
   319  	var httpClient *http.Client
   320  	subpath, _ := utils.GetSubpathFromConfig(a.Config())
   321  	siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL)
   322  	if (inURL.Hostname() == "localhost" || inURL.Hostname() == "127.0.0.1" || inURL.Hostname() == siteURL.Hostname()) && strings.HasPrefix(inURL.Path, path.Join(subpath, "plugins")) {
   323  		req.Header.Set(model.HEADER_AUTH, "Bearer "+a.Session().Token)
   324  		httpClient = a.HTTPService().MakeClient(true)
   325  	} else {
   326  		httpClient = a.HTTPService().MakeClient(false)
   327  	}
   328  
   329  	resp, httpErr := httpClient.Do(req)
   330  	if httpErr != nil {
   331  		return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err="+httpErr.Error(), http.StatusBadRequest)
   332  	}
   333  
   334  	if resp.StatusCode != http.StatusOK {
   335  		return resp, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, fmt.Sprintf("status=%v", resp.StatusCode), http.StatusBadRequest)
   336  	}
   337  
   338  	return resp, nil
   339  }
   340  
   341  type LocalResponseWriter struct {
   342  	data    []byte
   343  	headers http.Header
   344  	status  int
   345  }
   346  
   347  func (w *LocalResponseWriter) Header() http.Header {
   348  	if w.headers == nil {
   349  		w.headers = make(http.Header)
   350  	}
   351  	return w.headers
   352  }
   353  
   354  func (w *LocalResponseWriter) Write(bytes []byte) (int, error) {
   355  	w.data = make([]byte, len(bytes))
   356  	copy(w.data, bytes)
   357  	return len(w.data), nil
   358  }
   359  
   360  func (w *LocalResponseWriter) WriteHeader(statusCode int) {
   361  	w.status = statusCode
   362  }
   363  
   364  func (a *App) doPluginRequest(method, rawURL string, values url.Values, body []byte) (*http.Response, *model.AppError) {
   365  	rawURL = strings.TrimPrefix(rawURL, "/")
   366  	inURL, err := url.Parse(rawURL)
   367  	if err != nil {
   368  		return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
   369  	}
   370  	result := strings.Split(inURL.Path, "/")
   371  	if len(result) < 2 {
   372  		return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err=Unable to find pluginId", http.StatusBadRequest)
   373  	}
   374  	if result[0] != "plugins" {
   375  		return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err=plugins not in path", http.StatusBadRequest)
   376  	}
   377  	pluginId := result[1]
   378  
   379  	path := strings.TrimPrefix(inURL.Path, "plugins/"+pluginId)
   380  
   381  	base, err := url.Parse(path)
   382  	if err != nil {
   383  		return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
   384  	}
   385  
   386  	// merge the rawQuery params (if any) with the function's provided values
   387  	rawValues := inURL.Query()
   388  	if len(rawValues) != 0 {
   389  		if values == nil {
   390  			values = make(url.Values)
   391  		}
   392  		for k, vs := range rawValues {
   393  			for _, v := range vs {
   394  				values.Add(k, v)
   395  			}
   396  		}
   397  	}
   398  	if values != nil {
   399  		base.RawQuery = values.Encode()
   400  	}
   401  
   402  	w := &LocalResponseWriter{}
   403  	r, err := http.NewRequest(method, base.String(), bytes.NewReader(body))
   404  	if err != nil {
   405  		return nil, model.NewAppError("doPluginRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
   406  	}
   407  	r.Header.Set("Mattermost-User-Id", a.Session().UserId)
   408  	r.Header.Set(model.HEADER_AUTH, "Bearer "+a.Session().Token)
   409  	params := make(map[string]string)
   410  	params["plugin_id"] = pluginId
   411  	r = mux.SetURLVars(r, params)
   412  
   413  	a.ServePluginRequest(w, r)
   414  
   415  	resp := &http.Response{
   416  		StatusCode: w.status,
   417  		Proto:      "HTTP/1.1",
   418  		ProtoMajor: 1,
   419  		ProtoMinor: 1,
   420  		Header:     w.headers,
   421  		Body:       ioutil.NopCloser(bytes.NewReader(w.data)),
   422  	}
   423  	if resp.StatusCode == 0 {
   424  		resp.StatusCode = http.StatusOK
   425  	}
   426  
   427  	return resp, nil
   428  }
   429  
   430  func (a *App) doLocalWarnMetricsRequest(rawURL string, upstreamRequest *model.PostActionIntegrationRequest) *model.AppError {
   431  	_, err := url.Parse(rawURL)
   432  	if err != nil {
   433  		return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, err.Error(), http.StatusBadRequest)
   434  	}
   435  
   436  	warnMetricId := filepath.Base(rawURL)
   437  	if warnMetricId == "" {
   438  		return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest)
   439  	}
   440  
   441  	license := a.Srv().License()
   442  	if license != nil {
   443  		mlog.Debug("License is present, skip this call")
   444  		return nil
   445  	}
   446  
   447  	user, appErr := a.GetUser(a.Session().UserId)
   448  	if appErr != nil {
   449  		return appErr
   450  	}
   451  
   452  	botPost := &model.Post{
   453  		UserId:       upstreamRequest.Context["bot_user_id"].(string),
   454  		ChannelId:    upstreamRequest.ChannelId,
   455  		HasReactions: true,
   456  	}
   457  
   458  	isE0Edition := (model.BuildEnterpriseReady == "true") // license == nil was already validated upstream
   459  	_, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, utils.T, isE0Edition)
   460  	botPost.Message = ":white_check_mark: " + warnMetricDisplayTexts.BotSuccessMessage
   461  
   462  	if isE0Edition {
   463  		if appErr = a.RequestLicenseAndAckWarnMetric(warnMetricId, true); appErr != nil {
   464  			botPost.Message = ":warning: " + utils.T("api.server.warn_metric.bot_response.start_trial_failure.message")
   465  		}
   466  	} else {
   467  		forceAck := upstreamRequest.Context["force_ack"].(bool)
   468  		if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, user, forceAck, true); appErr != nil {
   469  			if forceAck {
   470  				return appErr
   471  			}
   472  			mailtoLinkText := a.buildWarnMetricMailtoLink(warnMetricId, user)
   473  			botPost.Message = ":warning: " + utils.T("api.server.warn_metric.bot_response.notification_failure.message")
   474  			actions := []*model.PostAction{}
   475  			actions = append(actions,
   476  				&model.PostAction{
   477  					Id:   "emailUs",
   478  					Name: utils.T("api.server.warn_metric.email_us"),
   479  					Type: model.POST_ACTION_TYPE_BUTTON,
   480  					Options: []*model.PostActionOptions{
   481  						{
   482  							Text:  "WarnMetricMailtoUrl",
   483  							Value: mailtoLinkText,
   484  						},
   485  						{
   486  							Text:  "TrackEventId",
   487  							Value: warnMetricId,
   488  						},
   489  					},
   490  					Integration: &model.PostActionIntegration{
   491  						Context: model.StringInterface{
   492  							"bot_user_id": botPost.UserId,
   493  							"force_ack":   true,
   494  						},
   495  						URL: fmt.Sprintf("/warn_metrics/ack/%s", model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500),
   496  					},
   497  				},
   498  			)
   499  			attachements := []*model.SlackAttachment{{
   500  				AuthorName: "",
   501  				Title:      "",
   502  				Actions:    actions,
   503  				Text:       utils.T("api.server.warn_metric.bot_response.notification_failure.body"),
   504  			}}
   505  			model.ParseSlackAttachment(botPost, attachements)
   506  		}
   507  	}
   508  
   509  	if _, err := a.CreatePostAsUser(botPost, a.Session().Id, true); err != nil {
   510  		return err
   511  	}
   512  
   513  	return nil
   514  }
   515  
   516  type MailToLinkContent struct {
   517  	MetricId      string `json:"metric_id"`
   518  	MailRecipient string `json:"mail_recipient"`
   519  	MailCC        string `json:"mail_cc"`
   520  	MailSubject   string `json:"mail_subject"`
   521  	MailBody      string `json:"mail_body"`
   522  }
   523  
   524  func (mlc *MailToLinkContent) ToJson() string {
   525  	b, _ := json.Marshal(mlc)
   526  	return string(b)
   527  }
   528  
   529  func (a *App) buildWarnMetricMailtoLink(warnMetricId string, user *model.User) string {
   530  	T := utils.GetUserTranslations(user.Locale)
   531  	_, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false)
   532  
   533  	mailBody := warnMetricDisplayTexts.EmailBody
   534  	mailBody += T("api.server.warn_metric.bot_response.mailto_contact_header", map[string]interface{}{"Contact": user.GetFullName()})
   535  	mailBody += "\r\n"
   536  	mailBody += T("api.server.warn_metric.bot_response.mailto_email_header", map[string]interface{}{"Email": user.Email})
   537  	mailBody += "\r\n"
   538  
   539  	registeredUsersCount, err := a.Srv().Store.User().Count(model.UserCountOptions{})
   540  	if err != nil {
   541  		mlog.Warn("Error retrieving the number of registered users", mlog.Err(err))
   542  	} else {
   543  		mailBody += utils.T("api.server.warn_metric.bot_response.mailto_registered_users_header", map[string]interface{}{"NoRegisteredUsers": registeredUsersCount})
   544  		mailBody += "\r\n"
   545  	}
   546  
   547  	mailBody += T("api.server.warn_metric.bot_response.mailto_site_url_header", map[string]interface{}{"SiteUrl": a.GetSiteURL()})
   548  	mailBody += "\r\n"
   549  
   550  	mailBody += T("api.server.warn_metric.bot_response.mailto_diagnostic_id_header", map[string]interface{}{"DiagnosticId": a.TelemetryId()})
   551  	mailBody += "\r\n"
   552  
   553  	mailBody += T("api.server.warn_metric.bot_response.mailto_footer")
   554  
   555  	mailToLinkContent := &MailToLinkContent{
   556  		MetricId:      warnMetricId,
   557  		MailRecipient: model.MM_SUPPORT_ADVISOR_ADDRESS,
   558  		MailCC:        user.Email,
   559  		MailSubject:   T("api.server.warn_metric.bot_response.mailto_subject"),
   560  		MailBody:      mailBody,
   561  	}
   562  
   563  	return mailToLinkContent.ToJson()
   564  }
   565  
   566  func (a *App) DoLocalRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
   567  	return a.doPluginRequest("POST", rawURL, nil, body)
   568  }
   569  
   570  func (a *App) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
   571  	clientTriggerId, userID, err := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
   572  	if err != nil {
   573  		return err
   574  	}
   575  
   576  	request.TriggerId = clientTriggerId
   577  
   578  	jsonRequest, _ := json.Marshal(request)
   579  
   580  	message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_OPEN_DIALOG, "", "", userID, nil)
   581  	message.Add("dialog", string(jsonRequest))
   582  	a.Publish(message)
   583  
   584  	return nil
   585  }
   586  
   587  func (a *App) SubmitInteractiveDialog(request model.SubmitDialogRequest) (*model.SubmitDialogResponse, *model.AppError) {
   588  	url := request.URL
   589  	request.URL = ""
   590  	request.Type = "dialog_submission"
   591  
   592  	b, jsonErr := json.Marshal(request)
   593  	if jsonErr != nil {
   594  		return nil, model.NewAppError("SubmitInteractiveDialog", "app.submit_interactive_dialog.json_error", nil, jsonErr.Error(), http.StatusBadRequest)
   595  	}
   596  
   597  	resp, err := a.DoActionRequest(url, b)
   598  	if err != nil {
   599  		return nil, err
   600  	}
   601  
   602  	defer resp.Body.Close()
   603  
   604  	var response model.SubmitDialogResponse
   605  	if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
   606  		// Don't fail, an empty response is acceptable
   607  		return &response, nil
   608  	}
   609  
   610  	return &response, nil
   611  }