summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/post_test.go39
-rw-r--r--api4/post_test.go156
-rw-r--r--app/webhook.go106
-rw-r--r--model/outgoing_webhook.go32
-rw-r--r--model/outgoing_webhook_test.go2
5 files changed, 271 insertions, 64 deletions
diff --git a/api/post_test.go b/api/post_test.go
index dea7afa39..c4e016b0b 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -177,8 +177,9 @@ func TestCreatePostWithCreateAt(t *testing.T) {
func testCreatePostWithOutgoingHook(
t *testing.T,
- hookContentType string,
- expectedContentType string,
+ hookContentType, expectedContentType, message, triggerWord string,
+ fileIds []string,
+ triggerWhen int,
) {
th := Setup().InitSystemAdmin()
Client := th.SystemAdminClient
@@ -221,7 +222,8 @@ func testCreatePostWithOutgoingHook(
UserName: user.Username,
PostId: post.Id,
Text: post.Message,
- TriggerWord: strings.Fields(post.Message)[0],
+ TriggerWord: triggerWord,
+ FileIds: strings.Join(post.FileIds, ","),
}
// depending on the Content-Type, we expect to find a JSON or form encoded payload
@@ -256,11 +258,17 @@ func testCreatePostWithOutgoingHook(
defer ts.Close()
// create an outgoing webhook, passing it the test server URL
- triggerWord := "bingo"
+ var triggerWords []string
+ if triggerWord != "" {
+ triggerWords = []string{triggerWord}
+ }
+
hook = &model.OutgoingWebhook{
ChannelId: channel.Id,
+ TeamId: team.Id,
ContentType: hookContentType,
- TriggerWords: []string{triggerWord},
+ TriggerWords: triggerWords,
+ TriggerWhen: triggerWhen,
CallbackURLs: []string{ts.URL},
}
@@ -271,10 +279,10 @@ func testCreatePostWithOutgoingHook(
}
// create a post to trigger the webhook
- message := triggerWord + " lorem ipusm"
post = &model.Post{
ChannelId: channel.Id,
Message: message,
+ FileIds: fileIds,
}
if result, err := Client.CreatePost(post); err != nil {
@@ -290,25 +298,34 @@ func testCreatePostWithOutgoingHook(
select {
case ok := <-success:
if !ok {
- t.Fatal("Test server was sent an invalid webhook.")
+ t.Fatal("Test server did send an invalid webhook.")
}
case <-time.After(time.Second):
- t.Fatal("Timeout, test server wasn't sent the webhook.")
+ t.Fatal("Timeout, test server did not send the webhook.")
}
}
func TestCreatePostWithOutgoingHook_form_urlencoded(t *testing.T) {
- testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded")
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "", "", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "", "", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
}
func TestCreatePostWithOutgoingHook_json(t *testing.T) {
- testCreatePostWithOutgoingHook(t, "application/json", "application/json")
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerword lorem ipsum", "triggerword", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_STARTS_WITH)
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerword lorem ipsum", "", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
}
// hooks created before we added the ContentType field should be considered as
// application/x-www-form-urlencoded
func TestCreatePostWithOutgoingHook_no_content_type(t *testing.T) {
- testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded")
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_STARTS_WITH)
}
func TestUpdatePost(t *testing.T) {
diff --git a/api4/post_test.go b/api4/post_test.go
index a2c0b065b..d554ca472 100644
--- a/api4/post_test.go
+++ b/api4/post_test.go
@@ -4,9 +4,13 @@
package api4
import (
+ "encoding/json"
"net/http"
+ "net/http/httptest"
+ "net/url"
"reflect"
"strconv"
+ "strings"
"testing"
"time"
@@ -101,6 +105,158 @@ func TestCreatePost(t *testing.T) {
}
}
+func testCreatePostWithOutgoingHook(
+ t *testing.T,
+ hookContentType, expectedContentType, message, triggerWord string,
+ fileIds []string,
+ triggerWhen int,
+) {
+ th := Setup().InitBasic().InitSystemAdmin()
+ defer TearDown()
+ user := th.SystemAdminUser
+ team := th.BasicTeam
+ channel := th.BasicChannel
+
+ enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks
+ enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks
+ utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks
+ utils.SetDefaultRolesBasedOnConfig()
+ }()
+ utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true
+ *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true
+ utils.SetDefaultRolesBasedOnConfig()
+
+ var hook *model.OutgoingWebhook
+ var post *model.Post
+
+ // Create a test server that is the target of the outgoing webhook. It will
+ // validate the webhook body fields and write to the success channel on
+ // success/failure.
+ success := make(chan bool)
+ wait := make(chan bool, 1)
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ <-wait
+
+ requestContentType := r.Header.Get("Content-Type")
+ if requestContentType != expectedContentType {
+ t.Logf("Content-Type is %s, should be %s", requestContentType, expectedContentType)
+ success <- false
+ return
+ }
+
+ expectedPayload := &model.OutgoingWebhookPayload{
+ Token: hook.Token,
+ TeamId: hook.TeamId,
+ TeamDomain: team.Name,
+ ChannelId: post.ChannelId,
+ ChannelName: channel.Name,
+ Timestamp: post.CreateAt,
+ UserId: post.UserId,
+ UserName: user.Username,
+ PostId: post.Id,
+ Text: post.Message,
+ TriggerWord: triggerWord,
+ FileIds: strings.Join(post.FileIds, ","),
+ }
+
+ // depending on the Content-Type, we expect to find a JSON or form encoded payload
+ if requestContentType == "application/json" {
+ decoder := json.NewDecoder(r.Body)
+ o := &model.OutgoingWebhookPayload{}
+ decoder.Decode(&o)
+
+ if !reflect.DeepEqual(expectedPayload, o) {
+ t.Logf("JSON payload is %+v, should be %+v", o, expectedPayload)
+ success <- false
+ return
+ }
+ } else {
+ err := r.ParseForm()
+ if err != nil {
+ t.Logf("Error parsing form: %q", err)
+ success <- false
+ return
+ }
+
+ expectedFormValues, _ := url.ParseQuery(expectedPayload.ToFormValues())
+ if !reflect.DeepEqual(expectedFormValues, r.Form) {
+ t.Logf("Form values are: %q\n, should be: %q\n", r.Form, expectedFormValues)
+ success <- false
+ return
+ }
+ }
+
+ success <- true
+ }))
+ defer ts.Close()
+
+ // create an outgoing webhook, passing it the test server URL
+ var triggerWords []string
+ if triggerWord != "" {
+ triggerWords = []string{triggerWord}
+ }
+
+ hook = &model.OutgoingWebhook{
+ ChannelId: channel.Id,
+ TeamId: team.Id,
+ ContentType: hookContentType,
+ TriggerWords: triggerWords,
+ TriggerWhen: triggerWhen,
+ CallbackURLs: []string{ts.URL},
+ }
+
+ hook, resp := th.SystemAdminClient.CreateOutgoingWebhook(hook)
+ CheckNoError(t, resp)
+
+ // create a post to trigger the webhook
+ post = &model.Post{
+ ChannelId: channel.Id,
+ Message: message,
+ FileIds: fileIds,
+ }
+
+ post, resp = th.SystemAdminClient.CreatePost(post)
+ CheckNoError(t, resp)
+
+ wait <- true
+
+ // We wait for the test server to write to the success channel and we make
+ // the test fail if that doesn't happen before the timeout.
+ select {
+ case ok := <-success:
+ if !ok {
+ t.Fatal("Test server did send an invalid webhook.")
+ }
+ case <-time.After(time.Second):
+ t.Fatal("Timeout, test server did not send the webhook.")
+ }
+}
+
+func TestCreatePostWithOutgoingHook_form_urlencoded(t *testing.T) {
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "", "", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/x-www-form-urlencoded", "application/x-www-form-urlencoded", "", "", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
+}
+
+func TestCreatePostWithOutgoingHook_json(t *testing.T) {
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerword lorem ipsum", "triggerword", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_STARTS_WITH)
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerword lorem ipsum", "", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "application/json", "application/json", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
+}
+
+// hooks created before we added the ContentType field should be considered as
+// application/x-www-form-urlencoded
+func TestCreatePostWithOutgoingHook_no_content_type(t *testing.T) {
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "triggerword", []string{"file_id_1"}, app.TRIGGERWORDS_STARTS_WITH)
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerword lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_EXACT_MATCH)
+ testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_STARTS_WITH)
+}
+
func TestUpdatePost(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer TearDown()
diff --git a/app/webhook.go b/app/webhook.go
index 6a7bb16e1..e92805608 100644
--- a/app/webhook.go
+++ b/app/webhook.go
@@ -18,8 +18,8 @@ import (
)
const (
- TRIGGERWORDS_FULL = 0
- TRIGGERWORDS_STARTSWITH = 1
+ TRIGGERWORDS_EXACT_MATCH = 0
+ TRIGGERWORDS_STARTS_WITH = 1
)
func handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
@@ -42,74 +42,80 @@ func handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Chan
return nil
}
+ var firstWord, triggerWord string
+
splitWords := strings.Fields(post.Message)
- if len(splitWords) == 0 {
- return nil
+ if len(splitWords) > 0 {
+ firstWord = splitWords[0]
}
- firstWord := splitWords[0]
relevantHooks := []*model.OutgoingWebhook{}
for _, hook := range hooks {
if hook.ChannelId == post.ChannelId || len(hook.ChannelId) == 0 {
if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 {
relevantHooks = append(relevantHooks, hook)
- } else if hook.TriggerWhen == TRIGGERWORDS_FULL && hook.HasTriggerWord(firstWord) {
+ triggerWord = ""
+ } else if hook.TriggerWhen == TRIGGERWORDS_EXACT_MATCH && hook.TriggerWordExactMatch(firstWord) {
relevantHooks = append(relevantHooks, hook)
- } else if hook.TriggerWhen == TRIGGERWORDS_STARTSWITH && hook.TriggerWordStartsWith(firstWord) {
+ triggerWord = hook.GetTriggerWord(firstWord, true)
+ } else if hook.TriggerWhen == TRIGGERWORDS_STARTS_WITH && hook.TriggerWordStartsWith(firstWord) {
relevantHooks = append(relevantHooks, hook)
+ triggerWord = hook.GetTriggerWord(firstWord, false)
}
}
}
for _, hook := range relevantHooks {
- go func(hook *model.OutgoingWebhook) {
- payload := &model.OutgoingWebhookPayload{
- Token: hook.Token,
- TeamId: hook.TeamId,
- TeamDomain: team.Name,
- ChannelId: post.ChannelId,
- ChannelName: channel.Name,
- Timestamp: post.CreateAt,
- UserId: post.UserId,
- UserName: user.Username,
- PostId: post.Id,
- Text: post.Message,
- TriggerWord: firstWord,
- }
- var body io.Reader
- var contentType string
- if hook.ContentType == "application/json" {
- body = strings.NewReader(payload.ToJSON())
- contentType = "application/json"
+ payload := &model.OutgoingWebhookPayload{
+ Token: hook.Token,
+ TeamId: hook.TeamId,
+ TeamDomain: team.Name,
+ ChannelId: post.ChannelId,
+ ChannelName: channel.Name,
+ Timestamp: post.CreateAt,
+ UserId: post.UserId,
+ UserName: user.Username,
+ PostId: post.Id,
+ Text: post.Message,
+ TriggerWord: triggerWord,
+ FileIds: strings.Join(post.FileIds, ","),
+ }
+ go TriggerWebhook(payload, hook, post)
+ }
+
+ return nil
+}
+
+func TriggerWebhook(payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post) {
+ var body io.Reader
+ var contentType string
+ if hook.ContentType == "application/json" {
+ body = strings.NewReader(payload.ToJSON())
+ contentType = "application/json"
+ } else {
+ body = strings.NewReader(payload.ToFormValues())
+ contentType = "application/x-www-form-urlencoded"
+ }
+
+ for _, url := range hook.CallbackURLs {
+ go func(url string) {
+ req, _ := http.NewRequest("POST", url, body)
+ req.Header.Set("Content-Type", contentType)
+ req.Header.Set("Accept", "application/json")
+ if resp, err := utils.HttpClient().Do(req); err != nil {
+ l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error())
} else {
- body = strings.NewReader(payload.ToFormValues())
- contentType = "application/x-www-form-urlencoded"
- }
+ defer CloseBody(resp)
+ respProps := model.MapFromJson(resp.Body)
- for _, url := range hook.CallbackURLs {
- go func(url string) {
- req, _ := http.NewRequest("POST", url, body)
- req.Header.Set("Content-Type", contentType)
- req.Header.Set("Accept", "application/json")
- if resp, err := utils.HttpClient().Do(req); err != nil {
- l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error())
- } else {
- defer CloseBody(resp)
- respProps := model.MapFromJson(resp.Body)
-
- if text, ok := respProps["text"]; ok {
- if _, err := CreateWebhookPost(hook.CreatorId, hook.TeamId, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
- l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.create_post.error"), err)
- }
- }
+ if text, ok := respProps["text"]; ok {
+ if _, err := CreateWebhookPost(hook.CreatorId, hook.TeamId, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
+ l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.create_post.error"), err)
}
- }(url)
+ }
}
-
- }(hook)
+ }(url)
}
-
- return nil
}
func CreateWebhookPost(userId, teamId, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) {
diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go
index 70c65bec7..59408c24e 100644
--- a/model/outgoing_webhook.go
+++ b/model/outgoing_webhook.go
@@ -42,6 +42,7 @@ type OutgoingWebhookPayload struct {
PostId string `json:"post_id"`
Text string `json:"text"`
TriggerWord string `json:"trigger_word"`
+ FileIds string `json:"file_ids"`
}
func (o *OutgoingWebhookPayload) ToJSON() string {
@@ -66,6 +67,7 @@ func (o *OutgoingWebhookPayload) ToFormValues() string {
v.Set("post_id", o.PostId)
v.Set("text", o.Text)
v.Set("trigger_word", o.TriggerWord)
+ v.Set("file_ids", o.FileIds)
return v.Encode()
}
@@ -198,8 +200,8 @@ func (o *OutgoingWebhook) PreUpdate() {
o.UpdateAt = GetMillis()
}
-func (o *OutgoingWebhook) HasTriggerWord(word string) bool {
- if len(o.TriggerWords) == 0 || len(word) == 0 {
+func (o *OutgoingWebhook) TriggerWordExactMatch(word string) bool {
+ if len(word) == 0 {
return false
}
@@ -213,7 +215,7 @@ func (o *OutgoingWebhook) HasTriggerWord(word string) bool {
}
func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool {
- if len(o.TriggerWords) == 0 || len(word) == 0 {
+ if len(word) == 0 {
return false
}
@@ -225,3 +227,27 @@ func (o *OutgoingWebhook) TriggerWordStartsWith(word string) bool {
return false
}
+
+func (o *OutgoingWebhook) GetTriggerWord(word string, isExactMatch bool) (triggerWord string) {
+ if len(word) == 0 {
+ return
+ }
+
+ if isExactMatch {
+ for _, trigger := range o.TriggerWords {
+ if trigger == word {
+ triggerWord = trigger
+ break
+ }
+ }
+ } else {
+ for _, trigger := range o.TriggerWords {
+ if strings.HasPrefix(word, trigger) {
+ triggerWord = trigger
+ break
+ }
+ }
+ }
+
+ return triggerWord
+}
diff --git a/model/outgoing_webhook_test.go b/model/outgoing_webhook_test.go
index 725423cdf..431b1f6c1 100644
--- a/model/outgoing_webhook_test.go
+++ b/model/outgoing_webhook_test.go
@@ -136,6 +136,7 @@ func TestOutgoingWebhookPayloadToFormValues(t *testing.T) {
PostId: "PostId",
Text: "Text",
TriggerWord: "TriggerWord",
+ FileIds: "FileIds",
}
v := url.Values{}
v.Set("token", "Token")
@@ -149,6 +150,7 @@ func TestOutgoingWebhookPayloadToFormValues(t *testing.T) {
v.Set("post_id", "PostId")
v.Set("text", "Text")
v.Set("trigger_word", "TriggerWord")
+ v.Set("file_ids", "FileIds")
if got, want := p.ToFormValues(), v.Encode(); !reflect.DeepEqual(got, want) {
t.Fatalf("Got %+v, wanted %+v", got, want)
}