Implement outgoing webhooks.
Этот коммит содержится в:
@@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
134
api/post.go
134
api/post.go
@@ -55,11 +55,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,18 +134,142 @@ 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())
|
fireAndForgetNotifications(rpost, c.Session.TeamId, c.GetSiteURL())
|
||||||
|
|
||||||
|
if triggerWebhooks {
|
||||||
|
fireAndForgetWebhookEvent(c, rpost)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpost, nil
|
return rpost, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fireAndForgetWebhookEvent(c *Context, post *model.Post) {
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
|
||||||
|
chchan := Srv.Store.Webhook().GetOutgoingByChannel(post.ChannelId)
|
||||||
|
|
||||||
|
firstWord := strings.Split(post.Message, " ")[0]
|
||||||
|
thchan := Srv.Store.Webhook().GetOutgoingByTriggerWord(c.Session.TeamId, post.ChannelId, firstWord)
|
||||||
|
|
||||||
|
hooks := []*model.OutgoingWebhook{}
|
||||||
|
|
||||||
|
if result := <-chchan; result.Err != nil {
|
||||||
|
l4g.Error("Encountered error getting webhook by channel, err=%v", result.Err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
hooks = append(hooks, result.Data.([]*model.OutgoingWebhook)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-thchan; result.Err != nil {
|
||||||
|
l4g.Error("Encountered error getting webhook by trigger word, err=%v", result.Err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
hooks = append(hooks, result.Data.([]*model.OutgoingWebhook)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cchan := Srv.Store.Channel().Get(post.ChannelId)
|
||||||
|
uchan := Srv.Store.User().Get(post.UserId)
|
||||||
|
|
||||||
|
tchan := make(chan *model.Team)
|
||||||
|
isTeamBeingFetched := make(map[string]bool)
|
||||||
|
for _, hook := range hooks {
|
||||||
|
if !isTeamBeingFetched[hook.TeamId] {
|
||||||
|
isTeamBeingFetched[hook.TeamId] = true
|
||||||
|
go func() {
|
||||||
|
if result := <-Srv.Store.Team().Get(hook.TeamId); result.Err != nil {
|
||||||
|
l4g.Error("Encountered error getting team, team_id=%s, err=%v", hook.TeamId, result.Err)
|
||||||
|
tchan <- nil
|
||||||
|
} else {
|
||||||
|
tchan <- result.Data.(*model.Team)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
teams := make(map[string]*model.Team)
|
||||||
|
for _, _ = range isTeamBeingFetched {
|
||||||
|
team := <-tchan
|
||||||
|
if team != nil {
|
||||||
|
teams[team.Id] = team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel *model.Channel
|
||||||
|
if result := <-cchan; result.Err != nil {
|
||||||
|
l4g.Error("Encountered error getting channel, channel_id=%s, err=%v", post.ChannelId, result.Err)
|
||||||
|
} else {
|
||||||
|
channel = result.Data.(*model.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)
|
||||||
|
} else {
|
||||||
|
user = result.Data.(*model.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hook := range hooks {
|
||||||
|
go func() {
|
||||||
|
p := url.Values{}
|
||||||
|
p.Set("token", hook.Token)
|
||||||
|
p.Set("team_id", hook.TeamId)
|
||||||
|
|
||||||
|
if team, ok := teams[hook.TeamId]; ok {
|
||||||
|
p.Set("team_domain", team.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Set("channel_id", post.ChannelId)
|
||||||
|
if channel != nil {
|
||||||
|
p.Set("channel_name", channel.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Set("timestamp", strconv.FormatInt(post.CreateAt/1000, 10))
|
||||||
|
|
||||||
|
p.Set("user_id", post.UserId)
|
||||||
|
if user != nil {
|
||||||
|
p.Set("user_name", user.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Set("text", post.Message)
|
||||||
|
if len(hook.TriggerWords) > 0 {
|
||||||
|
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)
|
||||||
|
|
||||||
|
if text, ok := respProps["text"]; ok {
|
||||||
|
respPost := &model.Post{Message: text, ChannelId: post.ChannelId}
|
||||||
|
if _, err := CreatePost(c, respPost, false); err != nil {
|
||||||
|
l4g.Error("Failed to create response post, err=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
|
func fireAndForgetNotifications(post *model.Post, teamId, siteURL string) {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
169
api/webhook.go
169
api/webhook.go
@@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
"GoogleDeveloperKey": "",
|
"GoogleDeveloperKey": "",
|
||||||
"EnableOAuthServiceProvider": false,
|
"EnableOAuthServiceProvider": false,
|
||||||
"EnableIncomingWebhooks": true,
|
"EnableIncomingWebhooks": true,
|
||||||
|
"EnableOutgoingWebhooks": true,
|
||||||
"EnablePostUsernameOverride": false,
|
"EnablePostUsernameOverride": false,
|
||||||
"EnablePostIconOverride": false,
|
"EnablePostIconOverride": false,
|
||||||
"EnableTesting": false,
|
"EnableTesting": false,
|
||||||
"EnableSecurityFixAlert": true
|
"EnableSecurityFixAlert": true
|
||||||
|
"EnableTesting": false
|
||||||
},
|
},
|
||||||
"TeamSettings": {
|
"TeamSettings": {
|
||||||
"SiteName": "Mattermost",
|
"SiteName": "Mattermost",
|
||||||
@@ -89,4 +91,4 @@
|
|||||||
"TokenEndpoint": "",
|
"TokenEndpoint": "",
|
||||||
"UserApiEndpoint": ""
|
"UserApiEndpoint": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
121
model/outgoing_webhook.go
Обычный файл
121
model/outgoing_webhook.go
Обычный файл
@@ -0,0 +1,121 @@
|
|||||||
|
// 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()
|
||||||
|
}
|
||||||
@@ -32,6 +32,10 @@ type Post struct {
|
|||||||
PendingPostId string `json:"pending_post_id" db:"-"`
|
PendingPostId string `json:"pending_post_id" db:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Something() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Post) ToJson() string {
|
func (o *Post) ToJson() string {
|
||||||
b, err := json.Marshal(o)
|
b, err := json.Marshal(o)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ import (
|
|||||||
"github.com/mattermost/platform/utils"
|
"github.com/mattermost/platform/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
INDEX_TYPE_FULL_TEXT = "full_text"
|
||||||
|
INDEX_TYPE_PATTERN = "pattern"
|
||||||
|
INDEX_TYPE_DEFAULT = "default"
|
||||||
|
)
|
||||||
|
|
||||||
type SqlStore struct {
|
type SqlStore struct {
|
||||||
master *gorp.DbMap
|
master *gorp.DbMap
|
||||||
replicas []*gorp.DbMap
|
replicas []*gorp.DbMap
|
||||||
@@ -363,14 +369,18 @@ 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) CreatePatternIndexIfNotExists(indexName string, tableName string, columnName string) {
|
||||||
|
ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_PATTERN)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,8 +390,10 @@ 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 if indexType == INDEX_TYPE_PATTERN {
|
||||||
|
query = "CREATE INDEX " + indexName + " ON " + tableName + " (" + columnName + " text_pattern_ops)"
|
||||||
} else {
|
} else {
|
||||||
query = "CREATE INDEX " + indexName + " ON " + tableName + " (" + columnName + ")"
|
query = "CREATE INDEX " + indexName + " ON " + tableName + " (" + columnName + ")"
|
||||||
}
|
}
|
||||||
@@ -406,7 +418,7 @@ func (ss SqlStore) createIndexIfNotExists(indexName string, tableName string, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
fullTextIndex := ""
|
fullTextIndex := ""
|
||||||
if fullText {
|
if indexType == INDEX_TYPE_FULL_TEXT || indexType == INDEX_TYPE_PATTERN {
|
||||||
fullTextIndex = " FULLTEXT "
|
fullTextIndex = " FULLTEXT "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mattermost/platform/model"
|
"github.com/mattermost/platform/model"
|
||||||
|
"github.com/mattermost/platform/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SqlWebhookStore struct {
|
type SqlWebhookStore struct {
|
||||||
@@ -20,6 +21,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 +39,11 @@ 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_channel_id", "OutgoingWebhooks", "ChannelId")
|
||||||
|
|
||||||
|
s.CreatePatternIndexIfNotExists("idx_outgoing_webhook_trigger_txt", "OutgoingWebhooks", "TriggerWords")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) StoreChannel {
|
func (s SqlWebhookStore) SaveIncoming(webhook *model.IncomingWebhook) StoreChannel {
|
||||||
@@ -126,3 +139,202 @@ 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) GetOutgoingByTriggerWord(teamId, channelId, triggerWord string) StoreChannel {
|
||||||
|
storeChannel := make(StoreChannel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
result := StoreResult{}
|
||||||
|
|
||||||
|
var webhooks []*model.OutgoingWebhook
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if utils.Cfg.SqlSettings.DriverName == "postgres" {
|
||||||
|
|
||||||
|
searchQuery := `SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
OutgoingWebhooks
|
||||||
|
WHERE
|
||||||
|
DeleteAt = 0
|
||||||
|
AND TeamId = $1
|
||||||
|
AND $2 LIKE '%' || TriggerWords || '%'`
|
||||||
|
|
||||||
|
if len(channelId) != 0 {
|
||||||
|
searchQuery += " AND (ChannelId = $3 OR ChannelId IS NULL)"
|
||||||
|
_, err = s.GetReplica().Select(&webhooks, searchQuery, teamId, triggerWord, channelId)
|
||||||
|
} else {
|
||||||
|
searchQuery += " AND ChannelId IS NULL"
|
||||||
|
_, err = s.GetReplica().Select(&webhooks, searchQuery, teamId, triggerWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if utils.Cfg.SqlSettings.DriverName == "mysql" {
|
||||||
|
searchQuery := `SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
OutgoingWebhooks
|
||||||
|
WHERE
|
||||||
|
DeleteAt = 0
|
||||||
|
AND TeamId = ?
|
||||||
|
AND MATCH (TriggerWords) AGAINST (? IN BOOLEAN MODE)`
|
||||||
|
|
||||||
|
triggerWord = "+" + triggerWord
|
||||||
|
|
||||||
|
if len(channelId) != 0 {
|
||||||
|
searchQuery += " AND (ChannelId = ? OR ChannelId = '')"
|
||||||
|
_, err = s.GetReplica().Select(&webhooks, searchQuery, teamId, triggerWord, channelId)
|
||||||
|
} else {
|
||||||
|
searchQuery += " AND ChannelId = ''"
|
||||||
|
_, err = s.GetReplica().Select(&webhooks, searchQuery, teamId, triggerWord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlPostStore.GetOutgoingByTriggerWord", "We encounted an error while getting the outgoing webhooks by trigger word", "teamId="+teamId+", channelId="+channelId+", triggerWord="+triggerWord+", 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
GetOutgoingByTriggerWord(teamId, channelId, triggerWord string) StoreChannel
|
||||||
|
DeleteOutgoing(webhookId string, time int64) StoreChannel
|
||||||
|
UpdateOutgoing(hook *model.OutgoingWebhook) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreferenceStore interface {
|
type PreferenceStore interface {
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ func getClientProperties(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'
|
||||||
|
|||||||
267
web/react/components/user_settings/manage_outgoing_hooks.jsx
Обычный файл
267
web/react/components/user_settings/manage_outgoing_hooks.jsx
Обычный файл
@@ -0,0 +1,267 @@
|
|||||||
|
// 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 = this.state.hooks;
|
||||||
|
if (!hooks) {
|
||||||
|
hooks = [];
|
||||||
|
}
|
||||||
|
hooks.push(data);
|
||||||
|
this.setState({hooks, serverError: null});
|
||||||
|
},
|
||||||
|
(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});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
this.setState({serverError: err});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
getHooks() {
|
||||||
|
Client.listOutgoingHooks(
|
||||||
|
(data) => {
|
||||||
|
const state = this.state;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
state.hooks = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.getHooksComplete = true;
|
||||||
|
this.setState(state);
|
||||||
|
},
|
||||||
|
(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) {
|
||||||
@@ -28,6 +29,7 @@ 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 (this.props.activeSection === 'incoming-hooks') {
|
||||||
@@ -40,10 +42,10 @@ export default class UserSettingsIntegrationsTab extends React.Component {
|
|||||||
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 {
|
||||||
@@ -52,9 +54,36 @@ export default class UserSettingsIntegrationsTab extends React.Component {
|
|||||||
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 (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');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,6 +111,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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,6 +114,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>",
|
||||||
|
|||||||
@@ -298,3 +298,7 @@
|
|||||||
.color-btn {
|
.color-btn {
|
||||||
margin:4px;
|
margin:4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-resize {
|
||||||
|
resize:none;
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user