summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author=Corey Hulen <corey@hulen.com>2015-10-19 11:42:40 -0700
committer=Corey Hulen <corey@hulen.com>2015-10-19 11:42:40 -0700
commit36658c13a4c7ebdfff22b8570e0db52bfa4de000 (patch)
treedc27373b42b8fe2a5f8ae88191924f27c361afb6
parent097d17bf2c4e07a153beb80afb15a546f291a418 (diff)
parentd139c9e825d0149329d90684ebe2d6b31a728b16 (diff)
downloadchat-36658c13a4c7ebdfff22b8570e0db52bfa4de000.tar.gz
chat-36658c13a4c7ebdfff22b8570e0db52bfa4de000.tar.bz2
chat-36658c13a4c7ebdfff22b8570e0db52bfa4de000.zip
Fixing merge conflicts
-rw-r--r--api/command.go2
-rw-r--r--api/post.go210
-rw-r--r--api/webhook.go169
-rw-r--r--api/webhook_test.go181
-rw-r--r--config/config.json1
-rw-r--r--doc/developer/Setup.md8
-rw-r--r--docker/dev/config_docker.json1
-rw-r--r--docker/local/config_docker.json1
-rw-r--r--model/client.go36
-rw-r--r--model/config.go1
-rw-r--r--model/incoming_webhook.go (renamed from model/webhook.go)0
-rw-r--r--model/incoming_webhook_test.go (renamed from model/webhook_test.go)0
-rw-r--r--model/outgoing_webhook.go135
-rw-r--r--model/outgoing_webhook_test.go97
-rw-r--r--store/sql_store.go15
-rw-r--r--store/sql_webhook_store.go171
-rw-r--r--store/sql_webhook_store_test.go206
-rw-r--r--store/store.go7
-rw-r--r--utils/config.go1
-rw-r--r--web/react/components/admin_console/service_settings.jsx36
-rw-r--r--web/react/components/team_signup_username_page.jsx2
-rw-r--r--web/react/components/user_settings/manage_outgoing_hooks.jsx262
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx89
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx3
-rw-r--r--web/react/utils/client.jsx58
-rw-r--r--web/react/utils/constants.jsx1
-rw-r--r--web/sass-files/sass/partials/_settings.scss4
-rw-r--r--web/web.go41
28 files changed, 1623 insertions, 115 deletions
diff --git a/api/command.go b/api/command.go
index 94b2cd2f8..52ff8fffd 100644
--- a/api/command.go
+++ b/api/command.go
@@ -145,7 +145,7 @@ func echoCommand(c *Context, command *model.Command) bool {
time.Sleep(time.Duration(delay) * time.Second)
- if _, err := CreatePost(c, post, false); err != nil {
+ if _, err := CreatePost(c, post, true); err != nil {
l4g.Error("Unable to create /echo post, err=%v", err)
}
}()
diff --git a/api/post.go b/api/post.go
index 58fd3488a..73a63cb72 100644
--- a/api/post.go
+++ b/api/post.go
@@ -13,6 +13,7 @@ import (
"net/http"
"net/url"
"path/filepath"
+ "regexp"
"strconv"
"strings"
"time"
@@ -55,11 +56,15 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
return
} else {
+ if result := <-Srv.Store.Channel().UpdateLastViewedAt(post.ChannelId, c.Session.UserId); result.Err != nil {
+ l4g.Error("Encountered error updating last viewed, channel_id=%s, user_id=%s, err=%v", post.ChannelId, c.Session.UserId, result.Err)
+ }
+
w.Write([]byte(rp.ToJson()))
}
}
-func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.Post, *model.AppError) {
+func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post, *model.AppError) {
var pchan store.StoreChannel
if len(post.RootId) > 0 {
pchan = Srv.Store.Post().Get(post.RootId)
@@ -130,50 +135,193 @@ func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.P
var rpost *model.Post
if result := <-Srv.Store.Post().Save(post); result.Err != nil {
return nil, result.Err
- } else if doUpdateLastViewed && (<-Srv.Store.Channel().UpdateLastViewedAt(post.ChannelId, c.Session.UserId)).Err != nil {
- return nil, result.Err
} else {
rpost = result.Data.(*model.Post)
- fireAndForgetNotifications(rpost, c.Session.TeamId, c.GetSiteURL())
+ handlePostEventsAndForget(c, rpost, triggerWebhooks)
}
return rpost, nil
}
-func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
+func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string) (*model.Post, *model.AppError) {
+ // parse links into Markdown format
+ linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
+ text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
+
+ linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
+ text = linkRegex.ReplaceAllString(text, "${1}")
+
+ post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text}
+ post.AddProp("from_webhook", "true")
+
+ if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
+ if len(overrideUsername) != 0 {
+ post.AddProp("override_username", overrideUsername)
+ } else {
+ post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
+ }
+ }
+ if utils.Cfg.ServiceSettings.EnablePostIconOverride {
+ if len(overrideIconUrl) != 0 {
+ post.AddProp("override_icon_url", overrideIconUrl)
+ } else {
+ post.AddProp("override_icon_url", model.DEFAULT_WEBHOOK_ICON)
+ }
+ }
+
+ if _, err := CreatePost(c, post, false); err != nil {
+ return nil, model.NewAppError("CreateWebhookPost", "Error creating post", "err="+err.Message)
+ }
+
+ return post, nil
+}
+
+func handlePostEventsAndForget(c *Context, post *model.Post, triggerWebhooks bool) {
go func() {
- // Get a list of user names (to be used as keywords) and ids for the given team
- uchan := Srv.Store.User().GetProfiles(teamId)
- echan := Srv.Store.Channel().GetMembers(post.ChannelId)
+ tchan := Srv.Store.Team().Get(c.Session.TeamId)
cchan := Srv.Store.Channel().Get(post.ChannelId)
- tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(post.UserId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ l4g.Error("Encountered error getting team, team_id=%s, err=%v", c.Session.TeamId, result.Err)
+ return
+ } else {
+ team = result.Data.(*model.Team)
+ }
var channel *model.Channel
- var channelName string
- var bodyText string
- var subjectText string
if result := <-cchan; result.Err != nil {
- l4g.Error("Failed to retrieve channel channel_id=%v, err=%v", post.ChannelId, result.Err)
+ l4g.Error("Encountered error getting channel, channel_id=%s, err=%v", post.ChannelId, result.Err)
return
} else {
channel = result.Data.(*model.Channel)
- if channel.Type == model.CHANNEL_DIRECT {
- bodyText = "You have one new message."
- subjectText = "New Direct Message"
- } else {
- bodyText = "You have one new mention."
- subjectText = "New Mention"
- channelName = channel.DisplayName
+ }
+
+ fireAndForgetNotifications(c, post, team, channel)
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ l4g.Error("Encountered error getting user, user_id=%s, err=%v", post.UserId, result.Err)
+ return
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if triggerWebhooks {
+ handleWebhookEventsAndForget(c, post, team, channel, user)
+ }
+ }()
+}
+
+func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) {
+ go func() {
+ hchan := Srv.Store.Webhook().GetOutgoingByTeam(c.Session.TeamId)
+
+ hooks := []*model.OutgoingWebhook{}
+
+ if result := <-hchan; result.Err != nil {
+ l4g.Error("Encountered error getting webhooks by team, err=%v", result.Err)
+ return
+ } else {
+ hooks = result.Data.([]*model.OutgoingWebhook)
+ }
+
+ if len(hooks) == 0 {
+ return
+ }
+
+ firstWord := strings.Split(post.Message, " ")[0]
+
+ relevantHooks := []*model.OutgoingWebhook{}
+
+ for _, hook := range hooks {
+ if hook.ChannelId == post.ChannelId {
+ if len(hook.TriggerWords) == 0 || hook.HasTriggerWord(firstWord) {
+ relevantHooks = append(relevantHooks, hook)
+ }
+ } else if len(hook.ChannelId) == 0 && hook.HasTriggerWord(firstWord) {
+ relevantHooks = append(relevantHooks, hook)
}
}
+ for _, hook := range relevantHooks {
+ go func() {
+ p := url.Values{}
+ p.Set("token", hook.Token)
+
+ p.Set("team_id", hook.TeamId)
+ p.Set("team_domain", team.Name)
+
+ p.Set("channel_id", post.ChannelId)
+ p.Set("channel_name", channel.Name)
+
+ p.Set("timestamp", strconv.FormatInt(post.CreateAt/1000, 10))
+
+ p.Set("user_id", post.UserId)
+ p.Set("user_name", user.Username)
+
+ p.Set("text", post.Message)
+ p.Set("trigger_word", firstWord)
+
+ client := &http.Client{}
+
+ for _, url := range hook.CallbackURLs {
+ go func() {
+ req, _ := http.NewRequest("POST", url, strings.NewReader(p.Encode()))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("Accept", "application/json")
+ if resp, err := client.Do(req); err != nil {
+ l4g.Error("Event POST failed, err=%s", err.Error())
+ } else {
+ respProps := model.MapFromJson(resp.Body)
+
+ // copy the context and create a mock session for posting the message
+ mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false}
+ newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL}
+
+ if text, ok := respProps["text"]; ok {
+ if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"]); err != nil {
+ l4g.Error("Failed to create response post, err=%v", err)
+ }
+ }
+ }
+ }()
+ }
+
+ }()
+ }
+
+ }()
+
+}
+
+func fireAndForgetNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) {
+
+ go func() {
+ // Get a list of user names (to be used as keywords) and ids for the given team
+ uchan := Srv.Store.User().GetProfiles(c.Session.TeamId)
+ echan := Srv.Store.Channel().GetMembers(post.ChannelId)
+
+ var channelName string
+ var bodyText string
+ var subjectText string
+ if channel.Type == model.CHANNEL_DIRECT {
+ bodyText = "You have one new message."
+ subjectText = "New Direct Message"
+ } else {
+ bodyText = "You have one new mention."
+ subjectText = "New Mention"
+ channelName = channel.DisplayName
+ }
+
var mentionedUsers []string
if result := <-uchan; result.Err != nil {
- l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", teamId, result.Err)
+ l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", c.Session.TeamId, result.Err)
return
} else {
profileMap := result.Data.(map[string]*model.User)
@@ -296,23 +444,15 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
mentionedUsers = append(mentionedUsers, k)
}
- var teamDisplayName string
- var teamURL string
- if result := <-tchan; result.Err != nil {
- l4g.Error("Failed to retrieve team team_id=%v, err=%v", teamId, result.Err)
- return
- } else {
- teamDisplayName = result.Data.(*model.Team).DisplayName
- teamURL = siteURL + "/" + result.Data.(*model.Team).Name
- }
+ teamURL := c.GetSiteURL() + "/" + team.Name
// Build and send the emails
location, _ := time.LoadLocation("UTC")
tm := time.Unix(post.CreateAt/1000, 0).In(location)
subjectPage := NewServerTemplatePage("post_subject")
- subjectPage.Props["SiteURL"] = siteURL
- subjectPage.Props["TeamDisplayName"] = teamDisplayName
+ subjectPage.Props["SiteURL"] = c.GetSiteURL()
+ subjectPage.Props["TeamDisplayName"] = team.DisplayName
subjectPage.Props["SubjectText"] = subjectText
subjectPage.Props["Month"] = tm.Month().String()[:3]
subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day())
@@ -330,9 +470,9 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
}
bodyPage := NewServerTemplatePage("post_body")
- bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Nickname"] = profileMap[id].FirstName
- bodyPage.Props["TeamDisplayName"] = teamDisplayName
+ bodyPage.Props["TeamDisplayName"] = team.DisplayName
bodyPage.Props["ChannelName"] = channelName
bodyPage.Props["BodyText"] = bodyText
bodyPage.Props["SenderName"] = senderName
@@ -399,7 +539,7 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
}
}
- message := model.NewMessage(teamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
+ message := model.NewMessage(c.Session.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
message.Add("post", post.ToJson())
if len(post.Filenames) != 0 {
diff --git a/api/webhook.go b/api/webhook.go
index de4ba6691..34c308879 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -18,6 +18,11 @@ func InitWebhook(r *mux.Router) {
sr.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST")
sr.Handle("/incoming/delete", ApiUserRequired(deleteIncomingHook)).Methods("POST")
sr.Handle("/incoming/list", ApiUserRequired(getIncomingHooks)).Methods("GET")
+
+ sr.Handle("/outgoing/create", ApiUserRequired(createOutgoingHook)).Methods("POST")
+ sr.Handle("/outgoing/regen_token", ApiUserRequired(regenOutgoingHookToken)).Methods("POST")
+ sr.Handle("/outgoing/delete", ApiUserRequired(deleteOutgoingHook)).Methods("POST")
+ sr.Handle("/outgoing/list", ApiUserRequired(getOutgoingHooks)).Methods("GET")
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -50,9 +55,11 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
channel = result.Data.(*model.Channel)
}
- if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
- c.LogAudit("fail - bad channel permissions")
- return
+ if !c.HasPermissionsToChannel(pchan, "createIncomingHook") {
+ if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.Session.TeamId {
+ c.LogAudit("fail - bad channel permissions")
+ return
+ }
}
if result := <-Srv.Store.Webhook().SaveIncoming(hook); result.Err != nil {
@@ -67,7 +74,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewAppError("deleteIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -87,7 +94,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
} else {
if c.Session.UserId != result.Data.(*model.IncomingWebhook).UserId && !c.IsTeamAdmin() {
- c.LogAudit("fail - inappropriate conditions")
+ c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewAppError("deleteIncomingHook", "Inappropriate permissions to delete incoming webhook", "user_id="+c.Session.UserId)
return
}
@@ -104,7 +111,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
- c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "")
+ c.Err = model.NewAppError("getIncomingHooks", "Incoming webhooks have been disabled by the system admin.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
@@ -117,3 +124,153 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.IncomingWebhookListToJson(hooks)))
}
}
+
+func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ c.Err = model.NewAppError("createOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ hook := model.OutgoingWebhookFromJson(r.Body)
+
+ if hook == nil {
+ c.SetInvalidParam("createOutgoingHook", "webhook")
+ return
+ }
+
+ hook.CreatorId = c.Session.UserId
+ hook.TeamId = c.Session.TeamId
+
+ if len(hook.ChannelId) != 0 {
+ cchan := Srv.Store.Channel().Get(hook.ChannelId)
+ pchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, hook.ChannelId, c.Session.UserId)
+
+ var channel *model.Channel
+ if result := <-cchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ if channel.Type != model.CHANNEL_OPEN {
+ c.LogAudit("fail - not open channel")
+ }
+
+ if !c.HasPermissionsToChannel(pchan, "createOutgoingHook") {
+ if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.Session.TeamId {
+ c.LogAudit("fail - bad channel permissions")
+ return
+ }
+ }
+ } else if len(hook.TriggerWords) == 0 {
+ c.Err = model.NewAppError("createOutgoingHook", "Either trigger_words or channel_id must be set", "")
+ return
+ }
+
+ if result := <-Srv.Store.Webhook().SaveOutgoing(hook); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ c.LogAudit("success")
+ rhook := result.Data.(*model.OutgoingWebhook)
+ w.Write([]byte(rhook.ToJson()))
+ }
+}
+
+func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ c.Err = model.NewAppError("getOutgoingHooks", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ if result := <-Srv.Store.Webhook().GetOutgoingByCreator(c.Session.UserId); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ hooks := result.Data.([]*model.OutgoingWebhook)
+ w.Write([]byte(model.OutgoingWebhookListToJson(hooks)))
+ }
+}
+
+func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewAppError("deleteOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ props := model.MapFromJson(r.Body)
+
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("deleteIncomingHook", "id")
+ return
+ }
+
+ if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ if c.Session.UserId != result.Data.(*model.OutgoingWebhook).CreatorId && !c.IsTeamAdmin() {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewAppError("deleteOutgoingHook", "Inappropriate permissions to delete outcoming webhook", "user_id="+c.Session.UserId)
+ return
+ }
+ }
+
+ if err := (<-Srv.Store.Webhook().DeleteOutgoing(id, model.GetMillis())).Err; err != nil {
+ c.Err = err
+ return
+ }
+
+ c.LogAudit("success")
+ w.Write([]byte(model.MapToJson(props)))
+}
+
+func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks {
+ c.Err = model.NewAppError("regenOutgoingHookToken", "Outgoing webhooks have been disabled by the system admin.", "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ props := model.MapFromJson(r.Body)
+
+ id := props["id"]
+ if len(id) == 0 {
+ c.SetInvalidParam("regenOutgoingHookToken", "id")
+ return
+ }
+
+ var hook *model.OutgoingWebhook
+ if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ hook = result.Data.(*model.OutgoingWebhook)
+
+ if c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewAppError("regenOutgoingHookToken", "Inappropriate permissions to regenerate outcoming webhook token", "user_id="+c.Session.UserId)
+ return
+ }
+ }
+
+ hook.Token = model.NewId()
+
+ if result := <-Srv.Store.Webhook().UpdateOutgoing(hook); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(result.Data.(*model.OutgoingWebhook).ToJson()))
+ }
+}
diff --git a/api/webhook_test.go b/api/webhook_test.go
index 16b9c9529..4c04a9922 100644
--- a/api/webhook_test.go
+++ b/api/webhook_test.go
@@ -152,6 +152,187 @@ func TestDeleteIncomingHook(t *testing.T) {
}
}
+func TestCreateOutgoingHook(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
+
+ hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ var rhook *model.OutgoingWebhook
+ if result, err := Client.CreateOutgoingWebhook(hook); err != nil {
+ t.Fatal(err)
+ } else {
+ rhook = result.Data.(*model.OutgoingWebhook)
+ }
+
+ if hook.ChannelId != rhook.ChannelId {
+ t.Fatal("channel ids didn't match")
+ }
+
+ if rhook.CreatorId != user.Id {
+ t.Fatal("user ids didn't match")
+ }
+
+ if rhook.TeamId != team.Id {
+ t.Fatal("team ids didn't match")
+ }
+
+ hook = &model.OutgoingWebhook{ChannelId: "junk", CallbackURLs: []string{"http://nowhere.com"}}
+ if _, err := Client.CreateOutgoingWebhook(hook); err == nil {
+ t.Fatal("should have failed - bad channel id")
+ }
+
+ hook = &model.OutgoingWebhook{ChannelId: channel2.Id, CreatorId: "123", TeamId: "456", CallbackURLs: []string{"http://nowhere.com"}}
+ if result, err := Client.CreateOutgoingWebhook(hook); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.OutgoingWebhook).CreatorId != user.Id {
+ t.Fatal("bad user id wasn't overwritten")
+ }
+ if result.Data.(*model.OutgoingWebhook).TeamId != team.Id {
+ t.Fatal("bad team id wasn't overwritten")
+ }
+ }
+ } else {
+ if _, err := Client.CreateOutgoingWebhook(hook); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
+func TestListOutgoingHooks(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ hook1 := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+ hook1 = Client.Must(Client.CreateOutgoingWebhook(hook1)).Data.(*model.OutgoingWebhook)
+
+ hook2 := &model.OutgoingWebhook{TriggerWords: []string{"trigger"}, CallbackURLs: []string{"http://nowhere.com"}}
+ hook2 = Client.Must(Client.CreateOutgoingWebhook(hook2)).Data.(*model.OutgoingWebhook)
+
+ if result, err := Client.ListOutgoingWebhooks(); err != nil {
+ t.Fatal(err)
+ } else {
+ hooks := result.Data.([]*model.OutgoingWebhook)
+
+ if len(hooks) != 2 {
+ t.Fatal("incorrect number of hooks")
+ }
+ }
+ } else {
+ if _, err := Client.ListOutgoingWebhooks(); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
+func TestDeleteOutgoingHook(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+ hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook)
+
+ data := make(map[string]string)
+ data["id"] = hook.Id
+
+ if _, err := Client.DeleteOutgoingWebhook(data); err != nil {
+ t.Fatal(err)
+ }
+
+ hooks := Client.Must(Client.ListOutgoingWebhooks()).Data.([]*model.OutgoingWebhook)
+ if len(hooks) != 0 {
+ t.Fatal("delete didn't work properly")
+ }
+ } else {
+ data := make(map[string]string)
+ data["id"] = "123"
+
+ if _, err := Client.DeleteOutgoingWebhook(data); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
+func TestRegenOutgoingHookToken(t *testing.T) {
+ Setup()
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
+ channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
+
+ if utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ hook := &model.OutgoingWebhook{ChannelId: channel1.Id, CallbackURLs: []string{"http://nowhere.com"}}
+ hook = Client.Must(Client.CreateOutgoingWebhook(hook)).Data.(*model.OutgoingWebhook)
+
+ data := make(map[string]string)
+ data["id"] = hook.Id
+
+ if result, err := Client.RegenOutgoingWebhookToken(data); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.OutgoingWebhook).Token == hook.Token {
+ t.Fatal("regen didn't work properly")
+ }
+ }
+
+ } else {
+ data := make(map[string]string)
+ data["id"] = "123"
+
+ if _, err := Client.RegenOutgoingWebhookToken(data); err == nil {
+ t.Fatal("should have errored - webhooks turned off")
+ }
+ }
+}
+
func TestZZWebSocketTearDown(t *testing.T) {
// *IMPORTANT* - Kind of hacky
// This should be the last function in any test file
diff --git a/config/config.json b/config/config.json
index 8ef151350..37109428d 100644
--- a/config/config.json
+++ b/config/config.json
@@ -6,6 +6,7 @@
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
+ "EnableOutgoingWebhooks": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/doc/developer/Setup.md b/doc/developer/Setup.md
index d806b7a9b..e78d4dff2 100644
--- a/doc/developer/Setup.md
+++ b/doc/developer/Setup.md
@@ -16,7 +16,9 @@ Developer Machine Setup
1. `mkdir ~/go`
2. Add the following to your ~/.bash_profile
`export GOPATH=$HOME/go`
- `export PATH=$PATH:$GOPATH/bin`
+ `export PATH=$PATH:$GOPATH/bin`
+ `ulimit -n 8096`
+ If you don't increase the file handle limit you may see some weird build issues with browserify or npm.
3. Reload your bash profile
`source ~/.bash_profile`
4. Install Node.js using Homebrew
@@ -57,7 +59,9 @@ Any issues? Please let us know on our forums at: http://forum.mattermost.org
2. Add the following to your ~/.bashrc
`export GOPATH=$HOME/go`
`export GOROOT=/usr/local/go`
- `export PATH=$PATH:$GOROOT/bin`
+ `export PATH=$PATH:$GOROOT/bin`
+ `ulimit -n 8096`
+ If you don't increase the file handle limit you may see some weird build issues with browserify or npm.
3. Reload your bashrc
`source ~/.bashrc`
6. Install Node.js
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index 653b6ffd7..00729395e 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -6,6 +6,7 @@
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
+ "EnableOutgoingWebhooks": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index 653b6ffd7..00729395e 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -6,6 +6,7 @@
"GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
+ "EnableOutgoingWebhooks": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"EnableTesting": false,
diff --git a/model/client.go b/model/client.go
index eea65c50e..9183dcacb 100644
--- a/model/client.go
+++ b/model/client.go
@@ -879,6 +879,42 @@ func (c *Client) GetPreferenceCategory(category string) (*Result, *AppError) {
}
}
+func (c *Client) CreateOutgoingWebhook(hook *OutgoingWebhook) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/hooks/outgoing/create", hook.ToJson()); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) DeleteOutgoingWebhook(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/hooks/outgoing/delete", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) ListOutgoingWebhooks() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/hooks/outgoing/list", "", ""); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookListFromJson(r.Body)}, nil
+ }
+}
+
+func (c *Client) RegenOutgoingWebhookToken(data map[string]string) (*Result, *AppError) {
+ if r, err := c.DoApiPost("/hooks/outgoing/regen_token", MapToJson(data)); err != nil {
+ return nil, err
+ } else {
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), OutgoingWebhookFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken
c.AuthType = HEADER_BEARER
diff --git a/model/config.go b/model/config.go
index 8a11b7bb7..ef76877c2 100644
--- a/model/config.go
+++ b/model/config.go
@@ -29,6 +29,7 @@ type ServiceSettings struct {
GoogleDeveloperKey string
EnableOAuthServiceProvider bool
EnableIncomingWebhooks bool
+ EnableOutgoingWebhooks bool
EnablePostUsernameOverride bool
EnablePostIconOverride bool
EnableTesting bool
diff --git a/model/webhook.go b/model/incoming_webhook.go
index 9b9969b96..9b9969b96 100644
--- a/model/webhook.go
+++ b/model/incoming_webhook.go
diff --git a/model/webhook_test.go b/model/incoming_webhook_test.go
index 5297d7d90..5297d7d90 100644
--- a/model/webhook_test.go
+++ b/model/incoming_webhook_test.go
diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go
new file mode 100644
index 000000000..8958dd5b0
--- /dev/null
+++ b/model/outgoing_webhook.go
@@ -0,0 +1,135 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+)
+
+type OutgoingWebhook struct {
+ Id string `json:"id"`
+ Token string `json:"token"`
+ CreateAt int64 `json:"create_at"`
+ UpdateAt int64 `json:"update_at"`
+ DeleteAt int64 `json:"delete_at"`
+ CreatorId string `json:"creator_id"`
+ ChannelId string `json:"channel_id"`
+ TeamId string `json:"team_id"`
+ TriggerWords StringArray `json:"trigger_words"`
+ CallbackURLs StringArray `json:"callback_urls"`
+}
+
+func (o *OutgoingWebhook) ToJson() string {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func OutgoingWebhookFromJson(data io.Reader) *OutgoingWebhook {
+ decoder := json.NewDecoder(data)
+ var o OutgoingWebhook
+ err := decoder.Decode(&o)
+ if err == nil {
+ return &o
+ } else {
+ return nil
+ }
+}
+
+func OutgoingWebhookListToJson(l []*OutgoingWebhook) string {
+ b, err := json.Marshal(l)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func OutgoingWebhookListFromJson(data io.Reader) []*OutgoingWebhook {
+ decoder := json.NewDecoder(data)
+ var o []*OutgoingWebhook
+ err := decoder.Decode(&o)
+ if err == nil {
+ return o
+ } else {
+ return nil
+ }
+}
+
+func (o *OutgoingWebhook) IsValid() *AppError {
+
+ if len(o.Id) != 26 {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid Id", "")
+ }
+
+ if len(o.Token) != 26 {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid token", "")
+ }
+
+ if o.CreateAt == 0 {
+ return NewAppError("OutgoingWebhook.IsValid", "Create at must be a valid time", "id="+o.Id)
+ }
+
+ if o.UpdateAt == 0 {
+ return NewAppError("OutgoingWebhook.IsValid", "Update at must be a valid time", "id="+o.Id)
+ }
+
+ if len(o.CreatorId) != 26 {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid user id", "")
+ }
+
+ if len(o.ChannelId) != 0 && len(o.ChannelId) != 26 {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid channel id", "")
+ }
+
+ if len(o.TeamId) != 26 {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid team id", "")
+ }
+
+ if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid trigger words", "")
+ }
+
+ if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 {
+ return NewAppError("OutgoingWebhook.IsValid", "Invalid callback urls", "")
+ }
+
+ return nil
+}
+
+func (o *OutgoingWebhook) PreSave() {
+ if o.Id == "" {
+ o.Id = NewId()
+ }
+
+ if o.Token == "" {
+ o.Token = NewId()
+ }
+
+ o.CreateAt = GetMillis()
+ o.UpdateAt = o.CreateAt
+}
+
+func (o *OutgoingWebhook) PreUpdate() {
+ o.UpdateAt = GetMillis()
+}
+
+func (o *OutgoingWebhook) HasTriggerWord(word string) bool {
+ if len(o.TriggerWords) == 0 || len(word) == 0 {
+ return false
+ }
+
+ for _, trigger := range o.TriggerWords {
+ if trigger == word {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/model/outgoing_webhook_test.go b/model/outgoing_webhook_test.go
new file mode 100644
index 000000000..2ca48c291
--- /dev/null
+++ b/model/outgoing_webhook_test.go
@@ -0,0 +1,97 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestOutgoingWebhookJson(t *testing.T) {
+ o := OutgoingWebhook{Id: NewId()}
+ json := o.ToJson()
+ ro := OutgoingWebhookFromJson(strings.NewReader(json))
+
+ if o.Id != ro.Id {
+ t.Fatal("Ids do not match")
+ }
+}
+
+func TestOutgoingWebhookIsValid(t *testing.T) {
+ o := OutgoingWebhook{}
+
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Id = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.UpdateAt = GetMillis()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreatorId = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CreatorId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Token = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.Token = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.ChannelId = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.ChannelId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.TeamId = "123"
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.TeamId = NewId()
+ if err := o.IsValid(); err == nil {
+ t.Fatal("should be invalid")
+ }
+
+ o.CallbackURLs = []string{"http://nowhere.com/"}
+ if err := o.IsValid(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestOutgoingWebhookPreSave(t *testing.T) {
+ o := OutgoingWebhook{}
+ o.PreSave()
+}
+
+func TestOutgoingWebhookPreUpdate(t *testing.T) {
+ o := OutgoingWebhook{}
+ o.PreUpdate()
+}
diff --git a/store/sql_store.go b/store/sql_store.go
index 692ac2664..c5bf840a1 100644
--- a/store/sql_store.go
+++ b/store/sql_store.go
@@ -30,6 +30,11 @@ import (
"github.com/mattermost/platform/utils"
)
+const (
+ INDEX_TYPE_FULL_TEXT = "full_text"
+ INDEX_TYPE_DEFAULT = "default"
+)
+
type SqlStore struct {
master *gorp.DbMap
replicas []*gorp.DbMap
@@ -363,14 +368,14 @@ func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) boo
// }
func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) {
- ss.createIndexIfNotExists(indexName, tableName, columnName, false)
+ ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT)
}
func (ss SqlStore) CreateFullTextIndexIfNotExists(indexName string, tableName string, columnName string) {
- ss.createIndexIfNotExists(indexName, tableName, columnName, true)
+ ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_FULL_TEXT)
}
-func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, fullText bool) {
+func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, columnName string, indexType string) {
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
_, err := ss.GetMaster().SelectStr("SELECT $1::regclass", indexName)
@@ -380,7 +385,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co
}
query := ""
- if fullText {
+ if indexType == INDEX_TYPE_FULL_TEXT {
query = "CREATE INDEX " + indexName + " ON " + tableName + " USING gin(to_tsvector('english', " + columnName + "))"
} else {
query = "CREATE INDEX " + indexName + " ON " + tableName + " (" + columnName + ")"
@@ -406,7 +411,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co
}
fullTextIndex := ""
- if fullText {
+ if indexType == INDEX_TYPE_FULL_TEXT {
fullTextIndex = " FULLTEXT "
}
diff --git a/store/sql_webhook_store.go b/store/sql_webhook_store.go
index 42a91a80e..1910984f0 100644
--- a/store/sql_webhook_store.go
+++ b/store/sql_webhook_store.go
@@ -20,6 +20,15 @@ func NewSqlWebhookStore(sqlStore *SqlStore) WebhookStore {
table.ColMap("UserId").SetMaxSize(26)
table.ColMap("ChannelId").SetMaxSize(26)
table.ColMap("TeamId").SetMaxSize(26)
+
+ tableo := db.AddTableWithName(model.OutgoingWebhook{}, "OutgoingWebhooks").SetKeys(false, "Id")
+ tableo.ColMap("Id").SetMaxSize(26)
+ tableo.ColMap("Token").SetMaxSize(26)
+ tableo.ColMap("CreatorId").SetMaxSize(26)
+ tableo.ColMap("ChannelId").SetMaxSize(26)
+ tableo.ColMap("TeamId").SetMaxSize(26)
+ tableo.ColMap("TriggerWords").SetMaxSize(1024)
+ tableo.ColMap("CallbackURLs").SetMaxSize(1024)
}
return s
@@ -29,8 +38,9 @@ func (s SqlWebhookStore) UpgradeSchemaIfNeeded() {
}
func (s SqlWebhookStore) CreateIndexesIfNotExists() {
- s.CreateIndexIfNotExists("idx_webhook_user_id", "IncomingWebhooks", "UserId")
- s.CreateIndexIfNotExists("idx_webhook_team_id", "IncomingWebhooks", "TeamId")
+ s.CreateIndexIfNotExists("idx_incoming_webhook_user_id", "IncomingWebhooks", "UserId")
+ s.CreateIndexIfNotExists("idx_incoming_webhook_team_id", "IncomingWebhooks", "TeamId")
+ s.CreateIndexIfNotExists("idx_outgoing_webhook_team_id", "OutgoingWebhooks", "TeamId")
}
func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) StoreChannel {
@@ -126,3 +136,160 @@ func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel {
return storeChannel
}
+
+func (s SqlWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if len(webhook.Id) > 0 {
+ result.Err = model.NewAppError("SqlWebhookStore.SaveOutgoing",
+ "You cannot overwrite an existing OutgoingWebhook", "id="+webhook.Id)
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ webhook.PreSave()
+ if result.Err = webhook.IsValid(); result.Err != nil {
+ storeChannel <- result
+ close(storeChannel)
+ return
+ }
+
+ if err := s.GetMaster().Insert(webhook); err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.SaveOutgoing", "We couldn't save the OutgoingWebhook", "id="+webhook.Id+", "+err.Error())
+ } else {
+ result.Data = webhook
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlWebhookStore) GetOutgoing(id string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var webhook model.OutgoingWebhook
+
+ if err := s.GetReplica().SelectOne(&webhook, "SELECT * FROM OutgoingWebhooks WHERE Id = :Id AND DeleteAt = 0", map[string]interface{}{"Id": id}); err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.GetOutgoing", "We couldn't get the webhook", "id="+id+", err="+err.Error())
+ }
+
+ result.Data = &webhook
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlWebhookStore) GetOutgoingByCreator(userId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var webhooks []*model.OutgoingWebhook
+
+ if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM OutgoingWebhooks WHERE CreatorId = :UserId AND DeleteAt = 0", map[string]interface{}{"UserId": userId}); err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.GetOutgoingByCreator", "We couldn't get the webhooks", "userId="+userId+", err="+err.Error())
+ }
+
+ result.Data = webhooks
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlWebhookStore) GetOutgoingByChannel(channelId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var webhooks []*model.OutgoingWebhook
+
+ if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM OutgoingWebhooks WHERE ChannelId = :ChannelId AND DeleteAt = 0", map[string]interface{}{"ChannelId": channelId}); err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.GetOutgoingByChannel", "We couldn't get the webhooks", "channelId="+channelId+", err="+err.Error())
+ }
+
+ result.Data = webhooks
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlWebhookStore) GetOutgoingByTeam(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var webhooks []*model.OutgoingWebhook
+
+ if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM OutgoingWebhooks WHERE TeamId = :TeamId AND DeleteAt = 0", map[string]interface{}{"TeamId": teamId}); err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.GetOutgoingByTeam", "We couldn't get the webhooks", "teamId="+teamId+", err="+err.Error())
+ }
+
+ result.Data = webhooks
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlWebhookStore) DeleteOutgoing(webhookId string, time int64) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := s.GetMaster().Exec("Update OutgoingWebhooks SET DeleteAt = :DeleteAt, UpdateAt = :UpdateAt WHERE Id = :Id", map[string]interface{}{"DeleteAt": time, "UpdateAt": time, "Id": webhookId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.DeleteOutgoing", "We couldn't delete the webhook", "id="+webhookId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlWebhookStore) UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ hook.UpdateAt = model.GetMillis()
+
+ if _, err := s.GetMaster().Update(hook); err != nil {
+ result.Err = model.NewAppError("SqlWebhookStore.UpdateOutgoing", "We couldn't update the webhook", "id="+hook.Id+", "+err.Error())
+ } else {
+ result.Data = hook
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_webhook_store_test.go b/store/sql_webhook_store_test.go
index 6f4ef4354..1fb990f3e 100644
--- a/store/sql_webhook_store_test.go
+++ b/store/sql_webhook_store_test.go
@@ -8,7 +8,7 @@ import (
"testing"
)
-func TestIncomingWebhookStoreSaveIncoming(t *testing.T) {
+func TestWebhookStoreSaveIncoming(t *testing.T) {
Setup()
o1 := model.IncomingWebhook{}
@@ -25,7 +25,7 @@ func TestIncomingWebhookStoreSaveIncoming(t *testing.T) {
}
}
-func TestIncomingWebhookStoreGetIncoming(t *testing.T) {
+func TestWebhookStoreGetIncoming(t *testing.T) {
Setup()
o1 := &model.IncomingWebhook{}
@@ -48,7 +48,34 @@ func TestIncomingWebhookStoreGetIncoming(t *testing.T) {
}
}
-func TestIncomingWebhookStoreDelete(t *testing.T) {
+func TestWebhookStoreGetIncomingByUser(t *testing.T) {
+ Setup()
+
+ o1 := &model.IncomingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.TeamId = model.NewId()
+
+ o1 = (<-store.Webhook().SaveIncoming(o1)).Data.(*model.IncomingWebhook)
+
+ if r1 := <-store.Webhook().GetIncomingByUser(o1.UserId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.([]*model.IncomingWebhook)[0].CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if result := <-store.Webhook().GetIncomingByUser("123"); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ if len(result.Data.([]*model.IncomingWebhook)) != 0 {
+ t.Fatal("no webhooks should have returned")
+ }
+ }
+}
+
+func TestWebhookStoreDeleteIncoming(t *testing.T) {
Setup()
o1 := &model.IncomingWebhook{}
@@ -75,3 +102,176 @@ func TestIncomingWebhookStoreDelete(t *testing.T) {
t.Fatal("Missing id should have failed")
}
}
+
+func TestWebhookStoreSaveOutgoing(t *testing.T) {
+ Setup()
+
+ o1 := model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ if err := (<-store.Webhook().SaveOutgoing(&o1)).Err; err != nil {
+ t.Fatal("couldn't save item", err)
+ }
+
+ if err := (<-store.Webhook().SaveOutgoing(&o1)).Err; err == nil {
+ t.Fatal("shouldn't be able to update from save")
+ }
+}
+
+func TestWebhookStoreGetOutgoing(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ if r1 := <-store.Webhook().GetOutgoing(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.OutgoingWebhook).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if err := (<-store.Webhook().GetOutgoing("123")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestWebhookStoreGetOutgoingByChannel(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ if r1 := <-store.Webhook().GetOutgoingByChannel(o1.ChannelId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.([]*model.OutgoingWebhook)[0].CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if result := <-store.Webhook().GetOutgoingByChannel("123"); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ if len(result.Data.([]*model.OutgoingWebhook)) != 0 {
+ t.Fatal("no webhooks should have returned")
+ }
+ }
+}
+
+func TestWebhookStoreGetOutgoingByCreator(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ if r1 := <-store.Webhook().GetOutgoingByCreator(o1.CreatorId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.([]*model.OutgoingWebhook)[0].CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if result := <-store.Webhook().GetOutgoingByCreator("123"); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ if len(result.Data.([]*model.OutgoingWebhook)) != 0 {
+ t.Fatal("no webhooks should have returned")
+ }
+ }
+}
+
+func TestWebhookStoreGetOutgoingByTeam(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ if r1 := <-store.Webhook().GetOutgoingByTeam(o1.TeamId); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.([]*model.OutgoingWebhook)[0].CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if result := <-store.Webhook().GetOutgoingByTeam("123"); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ if len(result.Data.([]*model.OutgoingWebhook)) != 0 {
+ t.Fatal("no webhooks should have returned")
+ }
+ }
+}
+
+func TestWebhookStoreDeleteOutgoing(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ if r1 := <-store.Webhook().GetOutgoing(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.OutgoingWebhook).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned webhook")
+ }
+ }
+
+ if r2 := <-store.Webhook().DeleteOutgoing(o1.Id, model.GetMillis()); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+
+ if r3 := (<-store.Webhook().GetOutgoing(o1.Id)); r3.Err == nil {
+ t.Log(r3.Data)
+ t.Fatal("Missing id should have failed")
+ }
+}
+
+func TestWebhookStoreUpdateOutgoing(t *testing.T) {
+ Setup()
+
+ o1 := &model.OutgoingWebhook{}
+ o1.ChannelId = model.NewId()
+ o1.CreatorId = model.NewId()
+ o1.TeamId = model.NewId()
+ o1.CallbackURLs = []string{"http://nowhere.com/"}
+
+ o1 = (<-store.Webhook().SaveOutgoing(o1)).Data.(*model.OutgoingWebhook)
+
+ o1.Token = model.NewId()
+
+ if r2 := <-store.Webhook().UpdateOutgoing(o1); r2.Err != nil {
+ t.Fatal(r2.Err)
+ }
+}
diff --git a/store/store.go b/store/store.go
index de335cc2b..70980a15c 100644
--- a/store/store.go
+++ b/store/store.go
@@ -150,6 +150,13 @@ type WebhookStore interface {
GetIncoming(id string) StoreChannel
GetIncomingByUser(userId string) StoreChannel
DeleteIncoming(webhookId string, time int64) StoreChannel
+ SaveOutgoing(webhook *model.OutgoingWebhook) StoreChannel
+ GetOutgoing(id string) StoreChannel
+ GetOutgoingByCreator(userId string) StoreChannel
+ GetOutgoingByChannel(channelId string) StoreChannel
+ GetOutgoingByTeam(teamId string) StoreChannel
+ DeleteOutgoing(webhookId string, time int64) StoreChannel
+ UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
}
type PreferenceStore interface {
diff --git a/utils/config.go b/utils/config.go
index 6a0cd0971..c1e3a8d2a 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -188,6 +188,7 @@ func getClientConfig(c *model.Config) map[string]string {
props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey
props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
+ props["EnableOutgoingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableOutgoingWebhooks)
props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 4105ba6da..53c89a942 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -36,6 +36,7 @@ export default class ServiceSettings extends React.Component {
config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnableOutgoingWebhooks = React.findDOMNode(this.refs.EnableOutgoingWebhooks).checked;
config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked;
config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
@@ -207,7 +208,40 @@ export default class ServiceSettings extends React.Component {
</div>
</div>
- <div className='form-group'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableOutgoingWebhooks'
+ >
+ {'Enable Outgoing Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOutgoingWebhooks'
+ value='true'
+ ref='EnableOutgoingWebhooks'
+ defaultChecked={this.props.config.ServiceSettings.EnableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOutgoingWebhooks'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableOutgoingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, outgoing webhooks will be allowed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='EnablePostUsernameOverride'
diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx
index 21e76e2b8..d8d0dbf2c 100644
--- a/web/react/components/team_signup_username_page.jsx
+++ b/web/react/components/team_signup_username_page.jsx
@@ -15,7 +15,7 @@ export default class TeamSignupUsernamePage extends React.Component {
}
submitBack(e) {
e.preventDefault();
- if (global.window.config.SendEmailNotifications === 'true') {
+ if (global.window.mm_config.SendEmailNotifications === 'true') {
this.props.state.wizard = 'send_invites';
} else {
this.props.state.wizard = 'team_url';
diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx
new file mode 100644
index 000000000..e83ae3bd6
--- /dev/null
+++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx
@@ -0,0 +1,262 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Constants = require('../../utils/constants.jsx');
+var ChannelStore = require('../../stores/channel_store.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+
+export default class ManageOutgoingHooks extends React.Component {
+ constructor() {
+ super();
+
+ this.getHooks = this.getHooks.bind(this);
+ this.addNewHook = this.addNewHook.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+ this.updateTriggerWords = this.updateTriggerWords.bind(this);
+ this.updateCallbackURLs = this.updateCallbackURLs.bind(this);
+
+ this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false};
+ }
+ componentDidMount() {
+ this.getHooks();
+ }
+ addNewHook(e) {
+ e.preventDefault();
+
+ if ((this.state.channelId === '' && this.state.triggerWords === '') ||
+ this.state.callbackURLs === '') {
+ return;
+ }
+
+ const hook = {};
+ hook.channel_id = this.state.channelId;
+ if (this.state.triggerWords.length !== 0) {
+ hook.trigger_words = this.state.triggerWords.trim().split(',');
+ }
+ hook.callback_urls = this.state.callbackURLs.split('\n');
+
+ Client.addOutgoingHook(
+ hook,
+ (data) => {
+ let hooks = Object.assign([], this.state.hooks);
+ if (!hooks) {
+ hooks = [];
+ }
+ hooks.push(data);
+ this.setState({hooks, serverError: null, channelId: '', triggerWords: '', callbackURLs: ''});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ removeHook(id) {
+ const data = {};
+ data.id = id;
+
+ Client.deleteOutgoingHook(
+ data,
+ () => {
+ const hooks = this.state.hooks;
+ let index = -1;
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ hooks.splice(index, 1);
+ }
+
+ this.setState({hooks});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ regenToken(id) {
+ const regenData = {};
+ regenData.id = id;
+
+ Client.regenOutgoingHookToken(
+ regenData,
+ (data) => {
+ const hooks = Object.assign([], this.state.hooks);
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ hooks[i] = data;
+ break;
+ }
+ }
+
+ this.setState({hooks, serverError: null});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ getHooks() {
+ Client.listOutgoingHooks(
+ (data) => {
+ if (data) {
+ this.setState({hooks: data, getHooksComplete: true, serverError: null});
+ }
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ updateChannelId(e) {
+ this.setState({channelId: e.target.value});
+ }
+ updateTriggerWords(e) {
+ this.setState({triggerWords: e.target.value});
+ }
+ updateCallbackURLs(e) {
+ this.setState({callbackURLs: e.target.value});
+ }
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <label className='has-error'>{this.state.serverError}</label>;
+ }
+
+ const channels = ChannelStore.getAll();
+ const options = [<option value=''>{'--- Select a channel ---'}</option>];
+ channels.forEach((channel) => {
+ if (channel.type === Constants.OPEN_CHANNEL) {
+ options.push(<option value={channel.id}>{channel.name}</option>);
+ }
+ });
+
+ const hooks = [];
+ this.state.hooks.forEach((hook) => {
+ const c = ChannelStore.get(hook.channel_id);
+ let channelDiv;
+ if (c) {
+ channelDiv = (
+ <div className='padding-top'>
+ <strong>{'Channel: '}</strong>{c.name}
+ </div>
+ );
+ }
+
+ let triggerDiv;
+ if (hook.trigger_words && hook.trigger_words.length !== 0) {
+ triggerDiv = (
+ <div className='padding-top'>
+ <strong>{'Trigger Words: '}</strong>{hook.trigger_words.join(', ')}
+ </div>
+ );
+ }
+
+ hooks.push(
+ <div className='font--small'>
+ <div className='padding-top x2 divider-light'></div>
+ <div className='padding-top x2'>
+ <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
+ </div>
+ {channelDiv}
+ {triggerDiv}
+ <div className='padding-top'>
+ <strong>{'Token: '}</strong>{hook.token}
+ </div>
+ <div className='padding-top'>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.regenToken.bind(this, hook.id)}
+ >
+ {'Regen Token'}
+ </a>
+ <span>{' - '}</span>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.removeHook.bind(this, hook.id)}
+ >
+ {'Remove'}
+ </a>
+ </div>
+ </div>
+ );
+ });
+
+ let displayHooks;
+ if (!this.state.getHooksComplete) {
+ displayHooks = <LoadingScreen/>;
+ } else if (hooks.length > 0) {
+ displayHooks = hooks;
+ } else {
+ displayHooks = <label>{': None'}</label>;
+ }
+
+ const existingHooks = (
+ <div className='padding-top x2'>
+ <label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label>
+ {displayHooks}
+ </div>
+ );
+
+ const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === '';
+
+ return (
+ <div key='addOutgoingHook'>
+ <label className='control-label'>{'Add a new outgoing webhook'}</label>
+ <div className='padding-top'>
+ <strong>{'Channel:'}</strong>
+ <select
+ ref='channelName'
+ className='form-control'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ >
+ {options}
+ </select>
+ <span>{'Only public channels can be used'}</span>
+ <br/>
+ <br/>
+ <strong>{'Trigger Words:'}</strong>
+ <input
+ ref='triggerWords'
+ className='form-control'
+ value={this.state.triggerWords}
+ onChange={this.updateTriggerWords}
+ placeholder='Optional if channel selected'
+ />
+ <span>{'Comma separated words to trigger on'}</span>
+ <br/>
+ <br/>
+ <strong>{'Callback URLs:'}</strong>
+ <textarea
+ ref='callbackURLs'
+ className='form-control no-resize'
+ value={this.state.callbackURLs}
+ resize={false}
+ rows={3}
+ onChange={this.updateCallbackURLs}
+ />
+ <span>{'New line separated URLs that will receive the HTTP POST event'}</span>
+ {serverError}
+ <div className='padding-top'>
+ <a
+ className={'btn btn-sm btn-primary'}
+ href='#'
+ disabled={disableButton}
+ onClick={this.addNewHook}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingHooks}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index 3be062ad3..1d9ea0ad5 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -4,6 +4,7 @@
var SettingItemMin = require('../setting_item_min.jsx');
var SettingItemMax = require('../setting_item_max.jsx');
var ManageIncomingHooks = require('./manage_incoming_hooks.jsx');
+var ManageOutgoingHooks = require('./manage_outgoing_hooks.jsx');
export default class UserSettingsIntegrationsTab extends React.Component {
constructor(props) {
@@ -19,6 +20,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
}
handleClose() {
this.updateSection('');
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
}
componentDidMount() {
$('#user_settings').on('hidden.bs.modal', this.handleClose);
@@ -28,35 +31,67 @@ export default class UserSettingsIntegrationsTab extends React.Component {
}
render() {
let incomingHooksSection;
+ let outgoingHooksSection;
var inputs = [];
- if (this.props.activeSection === 'incoming-hooks') {
- inputs.push(
- <ManageIncomingHooks />
- );
+ if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (this.props.activeSection === 'incoming-hooks') {
+ inputs.push(
+ <ManageIncomingHooks />
+ );
- incomingHooksSection = (
- <SettingItemMax
- title='Incoming Webhooks'
- width = 'full'
- inputs={inputs}
- updateSection={function clearSection(e) {
- this.updateSection('');
- e.preventDefault();
- }.bind(this)}
- />
- );
- } else {
- incomingHooksSection = (
- <SettingItemMin
- title='Incoming Webhooks'
- width = 'full'
- describe='Manage your incoming webhooks (Developer feature)'
- updateSection={function updateNameSection() {
- this.updateSection('incoming-hooks');
- }.bind(this)}
- />
- );
+ incomingHooksSection = (
+ <SettingItemMax
+ title='Incoming Webhooks'
+ width = 'full'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ incomingHooksSection = (
+ <SettingItemMin
+ title='Incoming Webhooks'
+ width = 'full'
+ describe='Manage your incoming webhooks (Developer feature)'
+ updateSection={() => {
+ this.updateSection('incoming-hooks');
+ }}
+ />
+ );
+ }
+ }
+
+ if (global.window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (this.props.activeSection === 'outgoing-hooks') {
+ inputs.push(
+ <ManageOutgoingHooks />
+ );
+
+ outgoingHooksSection = (
+ <SettingItemMax
+ title='Outgoing Webhooks'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ outgoingHooksSection = (
+ <SettingItemMin
+ title='Outgoing Webhooks'
+ describe='Manage your outgoing webhooks'
+ updateSection={() => {
+ this.updateSection('outgoing-hooks');
+ }}
+ />
+ );
+ }
}
return (
@@ -82,6 +117,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
<h3 className='tab-header'>{'Integration Settings'}</h3>
<div className='divider-dark first'/>
{incomingHooksSection}
+ <div className='divider-light'/>
+ {outgoingHooksSection}
<div className='divider-dark'/>
</div>
</div>
diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 4296d7909..5449ae91e 100644
--- a/web/react/components/user_settings/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -38,7 +38,8 @@ export default class UserSettingsModal extends React.Component {
if (global.window.mm_config.EnableOAuthServiceProvider === 'true') {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
}
- if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
+
+ if (global.window.mm_config.EnableIncomingWebhooks === 'true' || global.window.mm_config.EnableOutgoingWebhooks === 'true') {
tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
}
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 3130e9277..ee1f9ad27 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -1182,3 +1182,61 @@ export function savePreferences(preferences, success, error) {
}
});
}
+
+export function addOutgoingHook(hook, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(hook),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addOutgoingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteOutgoingHook(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteOutgoingHook', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listOutgoingHooks(success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/list',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listOutgoingHooks', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function regenOutgoingHookToken(data, success, error) {
+ $.ajax({
+ url: '/api/v1/hooks/outgoing/regen_token',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('regenOutgoingHookToken', xhr, status, err);
+ error(e);
+ }
+ });
+}
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 0911412d8..d891cc48b 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -125,6 +125,7 @@ module.exports = {
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20,
DM_CHANNEL: 'D',
+ OPEN_CHANNEL: 'O',
MAX_POST_LEN: 4000,
EMOJI_SIZE: 16,
ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>",
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 3146c16d5..c4591d7b6 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -291,3 +291,7 @@
.color-btn {
margin:4px;
}
+
+.no-resize {
+ resize:none;
+}
diff --git a/web/web.go b/web/web.go
index 2ae9bb344..701e36e95 100644
--- a/web/web.go
+++ b/web/web.go
@@ -15,8 +15,11 @@ import (
"gopkg.in/fsnotify.v1"
"html/template"
"net/http"
+<<<<<<< HEAD
"net/url"
"regexp"
+=======
+>>>>>>> master
"strconv"
"strings"
)
@@ -1007,9 +1010,6 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelName := props["channel"]
- overrideUsername := props["username"]
- overrideIconUrl := props["icon_url"]
-
var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message)
@@ -1038,12 +1038,8 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
}
- // parse links into Markdown format
- linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
- text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
-
- linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
- text = linkRegex.ReplaceAllString(text, "${1}")
+ overrideUsername := props["username"]
+ overrideIconUrl := props["icon_url"]
if result := <-cchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)
@@ -1054,35 +1050,16 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
- post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text}
- post.AddProp("from_webhook", "true")
-
- if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
- if len(overrideUsername) != 0 {
- post.AddProp("override_username", overrideUsername)
- } else {
- post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
- }
- }
-
- if utils.Cfg.ServiceSettings.EnablePostIconOverride {
- if len(overrideIconUrl) != 0 {
- post.AddProp("override_icon_url", overrideIconUrl)
- } else {
- post.AddProp("override_icon_url", model.DEFAULT_WEBHOOK_ICON)
- }
- }
+ // create a mock session
+ c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "")
return
}
- // create a mock session
- c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
-
- if _, err := api.CreatePost(c, post, false); err != nil {
- c.Err = model.NewAppError("incomingWebhook", "Error creating post", "err="+err.Message)
+ if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl); err != nil {
+ c.Err = err
return
}