Этот коммит содержится в:
=Corey Hulen
2015-10-19 11:42:40 -07:00
родитель 097d17bf2c d139c9e825
Коммит 36658c13a4
28 изменённых файлов: 1623 добавлений и 115 удалений

Просмотреть файл

@@ -145,7 +145,7 @@ func echoCommand(c *Context, command *model.Command) bool {
time.Sleep(time.Duration(delay) * time.Second) 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) l4g.Error("Unable to create /echo post, err=%v", err)
} }
}() }()

Просмотреть файл

@@ -13,6 +13,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -55,11 +56,15 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} else { } 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())) 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 var pchan store.StoreChannel
if len(post.RootId) > 0 { if len(post.RootId) > 0 {
pchan = Srv.Store.Post().Get(post.RootId) 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 var rpost *model.Post
if result := <-Srv.Store.Post().Save(post); result.Err != nil { if result := <-Srv.Store.Post().Save(post); result.Err != nil {
return nil, result.Err return nil, result.Err
} else if doUpdateLastViewed && (<-Srv.Store.Channel().UpdateLastViewedAt(post.ChannelId, c.Session.UserId)).Err != nil {
return nil, result.Err
} else { } else {
rpost = result.Data.(*model.Post) rpost = result.Data.(*model.Post)
fireAndForgetNotifications(rpost, c.Session.TeamId, c.GetSiteURL()) handlePostEventsAndForget(c, rpost, triggerWebhooks)
} }
return rpost, nil 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() { go func() {
// Get a list of user names (to be used as keywords) and ids for the given team tchan := Srv.Store.Team().Get(c.Session.TeamId)
uchan := Srv.Store.User().GetProfiles(teamId)
echan := Srv.Store.Channel().GetMembers(post.ChannelId)
cchan := Srv.Store.Channel().Get(post.ChannelId) 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 channel *model.Channel
var channelName string
var bodyText string
var subjectText string
if result := <-cchan; result.Err != nil { 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 return
} else { } else {
channel = result.Data.(*model.Channel) channel = result.Data.(*model.Channel)
if channel.Type == model.CHANNEL_DIRECT { }
bodyText = "You have one new message."
subjectText = "New Direct Message" fireAndForgetNotifications(c, post, team, channel)
} else {
bodyText = "You have one new mention." var user *model.User
subjectText = "New Mention" if result := <-uchan; result.Err != nil {
channelName = channel.DisplayName 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 var mentionedUsers []string
if result := <-uchan; result.Err != nil { 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 return
} else { } else {
profileMap := result.Data.(map[string]*model.User) profileMap := result.Data.(map[string]*model.User)
@@ -296,23 +444,15 @@ func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
mentionedUsers = append(mentionedUsers, k) mentionedUsers = append(mentionedUsers, k)
} }
var teamDisplayName string teamURL := c.GetSiteURL() + "/" + team.Name
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
}
// Build and send the emails // Build and send the emails
location, _ := time.LoadLocation("UTC") location, _ := time.LoadLocation("UTC")
tm := time.Unix(post.CreateAt/1000, 0).In(location) tm := time.Unix(post.CreateAt/1000, 0).In(location)
subjectPage := NewServerTemplatePage("post_subject") subjectPage := NewServerTemplatePage("post_subject")
subjectPage.Props["SiteURL"] = siteURL subjectPage.Props["SiteURL"] = c.GetSiteURL()
subjectPage.Props["TeamDisplayName"] = teamDisplayName subjectPage.Props["TeamDisplayName"] = team.DisplayName
subjectPage.Props["SubjectText"] = subjectText subjectPage.Props["SubjectText"] = subjectText
subjectPage.Props["Month"] = tm.Month().String()[:3] subjectPage.Props["Month"] = tm.Month().String()[:3]
subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) 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 := NewServerTemplatePage("post_body")
bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["SiteURL"] = c.GetSiteURL()
bodyPage.Props["Nickname"] = profileMap[id].FirstName bodyPage.Props["Nickname"] = profileMap[id].FirstName
bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["TeamDisplayName"] = team.DisplayName
bodyPage.Props["ChannelName"] = channelName bodyPage.Props["ChannelName"] = channelName
bodyPage.Props["BodyText"] = bodyText bodyPage.Props["BodyText"] = bodyText
bodyPage.Props["SenderName"] = senderName 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()) message.Add("post", post.ToJson())
if len(post.Filenames) != 0 { if len(post.Filenames) != 0 {

Просмотреть файл

@@ -18,6 +18,11 @@ func InitWebhook(r *mux.Router) {
sr.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST") sr.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST")
sr.Handle("/incoming/delete", ApiUserRequired(deleteIncomingHook)).Methods("POST") sr.Handle("/incoming/delete", ApiUserRequired(deleteIncomingHook)).Methods("POST")
sr.Handle("/incoming/list", ApiUserRequired(getIncomingHooks)).Methods("GET") 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) { 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) channel = result.Data.(*model.Channel)
} }
if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { if !c.HasPermissionsToChannel(pchan, "createIncomingHook") {
c.LogAudit("fail - bad channel permissions") if channel.Type != model.CHANNEL_OPEN || channel.TeamId != c.Session.TeamId {
return c.LogAudit("fail - bad channel permissions")
return
}
} }
if result := <-Srv.Store.Webhook().SaveIncoming(hook); result.Err != nil { 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) { func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { 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 c.Err.StatusCode = http.StatusNotImplemented
return return
} }
@@ -87,7 +94,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} else { } else {
if c.Session.UserId != result.Data.(*model.IncomingWebhook).UserId && !c.IsTeamAdmin() { 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) c.Err = model.NewAppError("deleteIncomingHook", "Inappropriate permissions to delete incoming webhook", "user_id="+c.Session.UserId)
return 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) { func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { 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 c.Err.StatusCode = http.StatusNotImplemented
return return
} }
@@ -117,3 +124,153 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.IncomingWebhookListToJson(hooks))) 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()))
}
}

Просмотреть файл

@@ -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) { func TestZZWebSocketTearDown(t *testing.T) {
// *IMPORTANT* - Kind of hacky // *IMPORTANT* - Kind of hacky
// This should be the last function in any test file // This should be the last function in any test file

Просмотреть файл

@@ -6,6 +6,7 @@
"GoogleDeveloperKey": "", "GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false, "EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true, "EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnablePostUsernameOverride": false, "EnablePostUsernameOverride": false,
"EnablePostIconOverride": false, "EnablePostIconOverride": false,
"EnableTesting": false, "EnableTesting": false,

Просмотреть файл

@@ -16,7 +16,9 @@ Developer Machine Setup
1. `mkdir ~/go` 1. `mkdir ~/go`
2. Add the following to your ~/.bash_profile 2. Add the following to your ~/.bash_profile
`export GOPATH=$HOME/go` `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 3. Reload your bash profile
`source ~/.bash_profile` `source ~/.bash_profile`
4. Install Node.js using Homebrew 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 2. Add the following to your ~/.bashrc
`export GOPATH=$HOME/go` `export GOPATH=$HOME/go`
`export GOROOT=/usr/local/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 3. Reload your bashrc
`source ~/.bashrc` `source ~/.bashrc`
6. Install Node.js 6. Install Node.js

Просмотреть файл

@@ -6,6 +6,7 @@
"GoogleDeveloperKey": "", "GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false, "EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true, "EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnablePostUsernameOverride": false, "EnablePostUsernameOverride": false,
"EnablePostIconOverride": false, "EnablePostIconOverride": false,
"EnableTesting": false, "EnableTesting": false,

Просмотреть файл

@@ -6,6 +6,7 @@
"GoogleDeveloperKey": "", "GoogleDeveloperKey": "",
"EnableOAuthServiceProvider": false, "EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true, "EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnablePostUsernameOverride": false, "EnablePostUsernameOverride": false,
"EnablePostIconOverride": false, "EnablePostIconOverride": false,
"EnableTesting": false, "EnableTesting": false,

Просмотреть файл

@@ -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) { func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken c.AuthToken = sessionToken
c.AuthType = HEADER_BEARER c.AuthType = HEADER_BEARER

Просмотреть файл

@@ -29,6 +29,7 @@ type ServiceSettings struct {
GoogleDeveloperKey string GoogleDeveloperKey string
EnableOAuthServiceProvider bool EnableOAuthServiceProvider bool
EnableIncomingWebhooks bool EnableIncomingWebhooks bool
EnableOutgoingWebhooks bool
EnablePostUsernameOverride bool EnablePostUsernameOverride bool
EnablePostIconOverride bool EnablePostIconOverride bool
EnableTesting bool EnableTesting bool

Просмотреть файл

Просмотреть файл

135
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
}

97
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()
}

Просмотреть файл

@@ -30,6 +30,11 @@ import (
"github.com/mattermost/platform/utils" "github.com/mattermost/platform/utils"
) )
const (
INDEX_TYPE_FULL_TEXT = "full_text"
INDEX_TYPE_DEFAULT = "default"
)
type SqlStore struct { type SqlStore struct {
master *gorp.DbMap master *gorp.DbMap
replicas []*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) { 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) { 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 { if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
_, err := ss.GetMaster().SelectStr("SELECT $1::regclass", indexName) _, err := ss.GetMaster().SelectStr("SELECT $1::regclass", indexName)
@@ -380,7 +385,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co
} }
query := "" query := ""
if fullText { if indexType == INDEX_TYPE_FULL_TEXT {
query = "CREATE INDEX " + indexName + " ON " + tableName + " USING gin(to_tsvector('english', " + columnName + "))" query = "CREATE INDEX " + indexName + " ON " + tableName + " USING gin(to_tsvector('english', " + columnName + "))"
} else { } else {
query = "CREATE INDEX " + indexName + " ON " + tableName + " (" + columnName + ")" query = "CREATE INDEX " + indexName + " ON " + tableName + " (" + columnName + ")"
@@ -406,7 +411,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co
} }
fullTextIndex := "" fullTextIndex := ""
if fullText { if indexType == INDEX_TYPE_FULL_TEXT {
fullTextIndex = " FULLTEXT " fullTextIndex = " FULLTEXT "
} }

Просмотреть файл

@@ -20,6 +20,15 @@ func NewSqlWebhookStore(sqlStore *SqlStore) WebhookStore {
table.ColMap("UserId").SetMaxSize(26) table.ColMap("UserId").SetMaxSize(26)
table.ColMap("ChannelId").SetMaxSize(26) table.ColMap("ChannelId").SetMaxSize(26)
table.ColMap("TeamId").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 return s
@@ -29,8 +38,9 @@ func (s SqlWebhookStore) UpgradeSchemaIfNeeded() {
} }
func (s SqlWebhookStore) CreateIndexesIfNotExists() { func (s SqlWebhookStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_webhook_user_id", "IncomingWebhooks", "UserId") s.CreateIndexIfNotExists("idx_incoming_webhook_user_id", "IncomingWebhooks", "UserId")
s.CreateIndexIfNotExists("idx_webhook_team_id", "IncomingWebhooks", "TeamId") 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 { func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) StoreChannel {
@@ -126,3 +136,160 @@ func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel {
return 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
}

Просмотреть файл

@@ -8,7 +8,7 @@ import (
"testing" "testing"
) )
func TestIncomingWebhookStoreSaveIncoming(t *testing.T) { func TestWebhookStoreSaveIncoming(t *testing.T) {
Setup() Setup()
o1 := model.IncomingWebhook{} o1 := model.IncomingWebhook{}
@@ -25,7 +25,7 @@ func TestIncomingWebhookStoreSaveIncoming(t *testing.T) {
} }
} }
func TestIncomingWebhookStoreGetIncoming(t *testing.T) { func TestWebhookStoreGetIncoming(t *testing.T) {
Setup() Setup()
o1 := &model.IncomingWebhook{} 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() Setup()
o1 := &model.IncomingWebhook{} o1 := &model.IncomingWebhook{}
@@ -75,3 +102,176 @@ func TestIncomingWebhookStoreDelete(t *testing.T) {
t.Fatal("Missing id should have failed") 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)
}
}

Просмотреть файл

@@ -150,6 +150,13 @@ type WebhookStore interface {
GetIncoming(id string) StoreChannel GetIncoming(id string) StoreChannel
GetIncomingByUser(userId string) StoreChannel GetIncomingByUser(userId string) StoreChannel
DeleteIncoming(webhookId string, time int64) 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 { type PreferenceStore interface {

Просмотреть файл

@@ -188,6 +188,7 @@ func getClientConfig(c *model.Config) map[string]string {
props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey
props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey
props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks) props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks)
props["EnableOutgoingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableOutgoingWebhooks)
props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride) props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride) props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)

Просмотреть файл

@@ -36,6 +36,7 @@ export default class ServiceSettings extends React.Component {
config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim(); config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim(); config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked; 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.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked; config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked;
config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked; config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
@@ -207,7 +208,40 @@ export default class ServiceSettings extends React.Component {
</div> </div>
</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 <label
className='control-label col-sm-4' className='control-label col-sm-4'
htmlFor='EnablePostUsernameOverride' htmlFor='EnablePostUsernameOverride'

Просмотреть файл

@@ -15,7 +15,7 @@ export default class TeamSignupUsernamePage extends React.Component {
} }
submitBack(e) { submitBack(e) {
e.preventDefault(); e.preventDefault();
if (global.window.config.SendEmailNotifications === 'true') { if (global.window.mm_config.SendEmailNotifications === 'true') {
this.props.state.wizard = 'send_invites'; this.props.state.wizard = 'send_invites';
} else { } else {
this.props.state.wizard = 'team_url'; this.props.state.wizard = 'team_url';

Просмотреть файл

@@ -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>
);
}
}

Просмотреть файл

@@ -4,6 +4,7 @@
var SettingItemMin = require('../setting_item_min.jsx'); var SettingItemMin = require('../setting_item_min.jsx');
var SettingItemMax = require('../setting_item_max.jsx'); var SettingItemMax = require('../setting_item_max.jsx');
var ManageIncomingHooks = require('./manage_incoming_hooks.jsx'); var ManageIncomingHooks = require('./manage_incoming_hooks.jsx');
var ManageOutgoingHooks = require('./manage_outgoing_hooks.jsx');
export default class UserSettingsIntegrationsTab extends React.Component { export default class UserSettingsIntegrationsTab extends React.Component {
constructor(props) { constructor(props) {
@@ -19,6 +20,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
} }
handleClose() { handleClose() {
this.updateSection(''); this.updateSection('');
$('.ps-container.modal-body').scrollTop(0);
$('.ps-container.modal-body').perfectScrollbar('update');
} }
componentDidMount() { componentDidMount() {
$('#user_settings').on('hidden.bs.modal', this.handleClose); $('#user_settings').on('hidden.bs.modal', this.handleClose);
@@ -28,35 +31,67 @@ export default class UserSettingsIntegrationsTab extends React.Component {
} }
render() { render() {
let incomingHooksSection; let incomingHooksSection;
let outgoingHooksSection;
var inputs = []; var inputs = [];
if (this.props.activeSection === 'incoming-hooks') { if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
inputs.push( if (this.props.activeSection === 'incoming-hooks') {
<ManageIncomingHooks /> inputs.push(
); <ManageIncomingHooks />
);
incomingHooksSection = ( incomingHooksSection = (
<SettingItemMax <SettingItemMax
title='Incoming Webhooks' title='Incoming Webhooks'
width = 'full' width = 'full'
inputs={inputs} inputs={inputs}
updateSection={function clearSection(e) { updateSection={(e) => {
this.updateSection(''); this.updateSection('');
e.preventDefault(); e.preventDefault();
}.bind(this)} }}
/> />
); );
} else { } else {
incomingHooksSection = ( incomingHooksSection = (
<SettingItemMin <SettingItemMin
title='Incoming Webhooks' title='Incoming Webhooks'
width = 'full' width = 'full'
describe='Manage your incoming webhooks (Developer feature)' describe='Manage your incoming webhooks (Developer feature)'
updateSection={function updateNameSection() { updateSection={() => {
this.updateSection('incoming-hooks'); this.updateSection('incoming-hooks');
}.bind(this)} }}
/> />
); );
}
}
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 ( return (
@@ -82,6 +117,8 @@ export default class UserSettingsIntegrationsTab extends React.Component {
<h3 className='tab-header'>{'Integration Settings'}</h3> <h3 className='tab-header'>{'Integration Settings'}</h3>
<div className='divider-dark first'/> <div className='divider-dark first'/>
{incomingHooksSection} {incomingHooksSection}
<div className='divider-light'/>
{outgoingHooksSection}
<div className='divider-dark'/> <div className='divider-dark'/>
</div> </div>
</div> </div>

Просмотреть файл

@@ -38,7 +38,8 @@ export default class UserSettingsModal extends React.Component {
if (global.window.mm_config.EnableOAuthServiceProvider === 'true') { if (global.window.mm_config.EnableOAuthServiceProvider === 'true') {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); 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: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
} }
tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'}); tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'});

Просмотреть файл

@@ -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);
}
});
}

Просмотреть файл

@@ -125,6 +125,7 @@ module.exports = {
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
MAX_DMS: 20, MAX_DMS: 20,
DM_CHANNEL: 'D', DM_CHANNEL: 'D',
OPEN_CHANNEL: 'O',
MAX_POST_LEN: 4000, MAX_POST_LEN: 4000,
EMOJI_SIZE: 16, 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>", 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>",

Просмотреть файл

@@ -291,3 +291,7 @@
.color-btn { .color-btn {
margin:4px; margin:4px;
} }
.no-resize {
resize:none;
}

Просмотреть файл

@@ -15,8 +15,11 @@ import (
"gopkg.in/fsnotify.v1" "gopkg.in/fsnotify.v1"
"html/template" "html/template"
"net/http" "net/http"
<<<<<<< HEAD
"net/url" "net/url"
"regexp" "regexp"
=======
>>>>>>> master
"strconv" "strconv"
"strings" "strings"
) )
@@ -1007,9 +1010,6 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
channelName := props["channel"] channelName := props["channel"]
overrideUsername := props["username"]
overrideIconUrl := props["icon_url"]
var hook *model.IncomingWebhook var hook *model.IncomingWebhook
if result := <-hchan; result.Err != nil { if result := <-hchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message) 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) cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
} }
// parse links into Markdown format overrideUsername := props["username"]
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) overrideIconUrl := props["icon_url"]
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
text = linkRegex.ReplaceAllString(text, "${1}")
if result := <-cchan; result.Err != nil { if result := <-cchan; result.Err != nil {
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message) 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) pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId)
post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text} // create a mock session
post.AddProp("from_webhook", "true") c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false}
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 !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN {
c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "") c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "")
return return
} }
// create a mock session if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl); err != nil {
c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} c.Err = err
if _, err := api.CreatePost(c, post, false); err != nil {
c.Err = model.NewAppError("incomingWebhook", "Error creating post", "err="+err.Message)
return return
} }