PLT-6965 jira integration (plus plugin scaffolding) (#6918)
* plugin scaffolding / jira integration * add vendored testify packages * webhook fix * don't change i18n ids * support configuration watching * add basic jira plugin configuration to admin console * fix eslint errors * fix another eslint warning * polish * undo unintentional config.json commit >:( * test fix * add jira plugin diagnostics, remove dm support, add bot tag, generate web-safe secrets * rebase, implement requested changes * requested changes * remove tests and minimize makefile change * add missing license headers * add missing comma * remove bad line from Makefile
Этот коммит содержится в:
@@ -1206,3 +1206,19 @@ func GetPinnedPosts(channelId string) (*model.PostList, *model.AppError) {
|
||||
return result.Data.(*model.PostList), nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
result := <-Srv.Store.Channel().GetByName("", model.GetDMNameFromIds(userId1, userId2), true)
|
||||
if result.Err != nil && result.Err.Id == store.MISSING_CHANNEL_ERROR {
|
||||
result := <-Srv.Store.Channel().CreateDirectChannel(userId1, userId2)
|
||||
if result.Err != nil {
|
||||
return nil, model.NewAppError("GetOrCreateDMChannel", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message, http.StatusBadRequest)
|
||||
}
|
||||
InvalidateCacheForUser(userId1)
|
||||
InvalidateCacheForUser(userId2)
|
||||
return result.Data.(*model.Channel), nil
|
||||
} else if result.Err != nil {
|
||||
return nil, model.NewAppError("GetOrCreateDMChannel", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode)
|
||||
}
|
||||
return result.Data.(*model.Channel), nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -38,6 +39,7 @@ const (
|
||||
TRACK_CONFIG_ANALYTICS = "config_analytics"
|
||||
TRACK_CONFIG_ANNOUNCEMENT = "config_announcement"
|
||||
TRACK_CONFIG_ELASTICSEARCH = "config_elasticsearch"
|
||||
TRACK_CONFIG_PLUGIN = "config_plugin"
|
||||
|
||||
TRACK_ACTIVITY = "activity"
|
||||
TRACK_LICENSE = "license"
|
||||
@@ -87,6 +89,23 @@ func isDefault(setting interface{}, defaultValue interface{}) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func pluginSetting(plugin, key string, defaultValue interface{}) interface{} {
|
||||
settings, ok := utils.Cfg.PluginSettings.Plugins[plugin]
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if b, err := json.Marshal(settings); err != nil {
|
||||
return defaultValue
|
||||
} else {
|
||||
json.Unmarshal(b, &m)
|
||||
}
|
||||
if value, ok := m[key]; ok {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func trackActivity() {
|
||||
var userCount int64
|
||||
var activeUserCount int64
|
||||
@@ -393,6 +412,10 @@ func trackConfig() {
|
||||
"enable_searching": *utils.Cfg.ElasticsearchSettings.EnableSearching,
|
||||
"sniff": *utils.Cfg.ElasticsearchSettings.Sniff,
|
||||
})
|
||||
|
||||
SendDiagnostic(TRACK_CONFIG_PLUGIN, map[string]interface{}{
|
||||
"enable_jira": pluginSetting("jira", "enabled", false),
|
||||
})
|
||||
}
|
||||
|
||||
func trackLicense() {
|
||||
|
||||
@@ -28,6 +28,24 @@ func newTestServer() (chan string, *httptest.Server) {
|
||||
return result, server
|
||||
}
|
||||
|
||||
func TestPluginSetting(t *testing.T) {
|
||||
before := utils.Cfg.PluginSettings.Plugins
|
||||
utils.Cfg.PluginSettings.Plugins = map[string]interface{}{
|
||||
"test": map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
defer func() {
|
||||
utils.Cfg.PluginSettings.Plugins = before
|
||||
}()
|
||||
if pluginSetting("test", "foo", "asd") != "bar" {
|
||||
t.Fatal()
|
||||
}
|
||||
if pluginSetting("test", "qwe", "asd") != "asd" {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnostics(t *testing.T) {
|
||||
Setup().InitBasic()
|
||||
|
||||
@@ -116,6 +134,7 @@ func TestDiagnostics(t *testing.T) {
|
||||
TRACK_CONFIG_SUPPORT,
|
||||
TRACK_CONFIG_NATIVEAPP,
|
||||
TRACK_CONFIG_ANALYTICS,
|
||||
TRACK_CONFIG_PLUGIN,
|
||||
TRACK_ACTIVITY,
|
||||
TRACK_SERVER,
|
||||
} {
|
||||
|
||||
37
app/plugin/api.go
Обычный файл
37
app/plugin/api.go
Обычный файл
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
// Loads the plugin's configuration
|
||||
LoadPluginConfiguration(dest interface{}) error
|
||||
|
||||
// The plugin's router
|
||||
PluginRouter() *mux.Router
|
||||
|
||||
// Gets a team by its name
|
||||
GetTeamByName(name string) (*model.Team, *model.AppError)
|
||||
|
||||
// Gets a user by its name
|
||||
GetUserByName(name string) (*model.User, *model.AppError)
|
||||
|
||||
// Gets a channel by its name
|
||||
GetChannelByName(teamId, name string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Gets a direct message channel
|
||||
GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Creates a post
|
||||
CreatePost(post *model.Post, teamId string) (*model.Post, *model.AppError)
|
||||
|
||||
// Returns a localized string. If a request is given, its headers will be used to pick a locale.
|
||||
I18n(id string, r *http.Request) string
|
||||
}
|
||||
9
app/plugin/base.go
Обычный файл
9
app/plugin/base.go
Обычный файл
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// Base provides default implementations for hooks.
|
||||
type Base struct{}
|
||||
|
||||
func (b *Base) OnConfigurationChange() {}
|
||||
10
app/plugin/hooks.go
Обычный файл
10
app/plugin/hooks.go
Обычный файл
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// All implementations should be safe for concurrent use.
|
||||
type Hooks interface {
|
||||
// Invoked when configuration changes may have been made
|
||||
OnConfigurationChange()
|
||||
}
|
||||
10
app/plugin/jira/configuration.go
Обычный файл
10
app/plugin/jira/configuration.go
Обычный файл
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package jira
|
||||
|
||||
type Configuration struct {
|
||||
Enabled bool
|
||||
Secret string
|
||||
UserName string
|
||||
}
|
||||
78
app/plugin/jira/plugin.go
Обычный файл
78
app/plugin/jira/plugin.go
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package jira
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/platform/app/plugin"
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
plugin.Base
|
||||
api plugin.API
|
||||
configuration atomic.Value
|
||||
}
|
||||
|
||||
func (p *Plugin) Initialize(api plugin.API) {
|
||||
p.api = api
|
||||
p.OnConfigurationChange()
|
||||
api.PluginRouter().HandleFunc("/webhook", p.handleWebhook).Methods("POST")
|
||||
}
|
||||
|
||||
func (p *Plugin) config() *Configuration {
|
||||
return p.configuration.Load().(*Configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnConfigurationChange() {
|
||||
var configuration Configuration
|
||||
if err := p.api.LoadPluginConfiguration(&configuration); err != nil {
|
||||
l4g.Error(err.Error())
|
||||
}
|
||||
p.configuration.Store(&configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
config := p.config()
|
||||
if !config.Enabled || config.Secret == "" || config.UserName == "" {
|
||||
http.Error(w, "This plugin is not configured.", http.StatusForbidden)
|
||||
return
|
||||
} else if subtle.ConstantTimeCompare([]byte(r.URL.Query().Get("secret")), []byte(config.Secret)) != 1 {
|
||||
http.Error(w, "You must provide the configured secret.", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var webhook Webhook
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if attachment, err := webhook.SlackAttachment(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
} else if attachment == nil {
|
||||
return
|
||||
} else if r.URL.Query().Get("channel") == "" {
|
||||
http.Error(w, "You must provide a channel.", http.StatusBadRequest)
|
||||
} else if user, err := p.api.GetUserByName(config.UserName); err != nil {
|
||||
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
|
||||
} else if team, err := p.api.GetTeamByName(r.URL.Query().Get("team")); err != nil {
|
||||
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
|
||||
} else if channel, err := p.api.GetChannelByName(team.Id, r.URL.Query().Get("channel")); err != nil {
|
||||
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
|
||||
} else if _, err := p.api.CreatePost(&model.Post{
|
||||
ChannelId: channel.Id,
|
||||
Type: model.POST_SLACK_ATTACHMENT,
|
||||
UserId: user.Id,
|
||||
Props: map[string]interface{}{
|
||||
"from_webhook": "true",
|
||||
"attachments": []*model.SlackAttachment{attachment},
|
||||
},
|
||||
}, channel.TeamId); err != nil {
|
||||
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
|
||||
}
|
||||
}
|
||||
168
app/plugin/jira/webhook.go
Обычный файл
168
app/plugin/jira/webhook.go
Обычный файл
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
type Webhook struct {
|
||||
WebhookEvent string
|
||||
Issue struct {
|
||||
Self string
|
||||
Key string
|
||||
Fields struct {
|
||||
Assignee *struct {
|
||||
DisplayName string
|
||||
Name string
|
||||
}
|
||||
Summary string
|
||||
Description string
|
||||
Priority *struct {
|
||||
Id string
|
||||
Name string
|
||||
}
|
||||
IssueType struct {
|
||||
Name string
|
||||
IconURL string
|
||||
}
|
||||
Resolution *struct {
|
||||
Id string
|
||||
}
|
||||
Status struct {
|
||||
Id string
|
||||
}
|
||||
}
|
||||
}
|
||||
User struct {
|
||||
Name string
|
||||
AvatarUrls map[string]string
|
||||
DisplayName string
|
||||
}
|
||||
Comment struct {
|
||||
Body string
|
||||
}
|
||||
ChangeLog struct {
|
||||
Items []struct {
|
||||
FromString string
|
||||
ToString string
|
||||
Field string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the text to be placed in the resulting post or an empty string if nothing should be
|
||||
// posted.
|
||||
func (w *Webhook) SlackAttachment() (*model.SlackAttachment, error) {
|
||||
switch w.WebhookEvent {
|
||||
case "jira:issue_created":
|
||||
case "jira:issue_updated":
|
||||
isResolutionChange := false
|
||||
for _, change := range w.ChangeLog.Items {
|
||||
if change.Field == "resolution" {
|
||||
isResolutionChange = (change.FromString == "") != (change.ToString == "")
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isResolutionChange {
|
||||
return nil, nil
|
||||
}
|
||||
case "jira:issue_deleted":
|
||||
if w.Issue.Fields.Resolution != nil {
|
||||
return nil, nil
|
||||
}
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pretext, err := w.renderText("" +
|
||||
"{{.User.DisplayName}} {{.Verb}} {{.Issue.Fields.IssueType.Name}} " +
|
||||
"[{{.Issue.Key}}]({{.JIRAURL}}/browse/{{.Issue.Key}})" +
|
||||
"")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
text, err := w.renderText("" +
|
||||
"[{{.Issue.Fields.Summary}}]({{.JIRAURL}}/browse/{{.Issue.Key}})" +
|
||||
"{{if eq .WebhookEvent \"jira:issue_created\"}}{{if ne .Issue.Fields.Description \"\"}}" +
|
||||
"\n\n{{.Issue.Fields.Description}}" +
|
||||
"{{end}}{{end}}" +
|
||||
"")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fields []*model.SlackAttachmentField
|
||||
if w.WebhookEvent == "jira:issue_created" {
|
||||
if w.Issue.Fields.Assignee != nil {
|
||||
fields = append(fields, &model.SlackAttachmentField{
|
||||
Title: "Assignee",
|
||||
Value: w.Issue.Fields.Assignee.DisplayName,
|
||||
Short: true,
|
||||
})
|
||||
}
|
||||
if w.Issue.Fields.Priority != nil {
|
||||
fields = append(fields, &model.SlackAttachmentField{
|
||||
Title: "Priority",
|
||||
Value: w.Issue.Fields.Priority.Name,
|
||||
Short: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &model.SlackAttachment{
|
||||
Fallback: pretext,
|
||||
Color: "#95b7d0",
|
||||
Pretext: pretext,
|
||||
Text: text,
|
||||
Fields: fields,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Webhook) renderText(tplBody string) (string, error) {
|
||||
issueSelf, err := url.Parse(w.Issue.Self)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jiraURL := strings.TrimRight(issueSelf.ResolveReference(&url.URL{Path: "/"}).String(), "/")
|
||||
verb := strings.TrimPrefix(w.WebhookEvent, "jira:issue_")
|
||||
|
||||
if w.WebhookEvent == "jira:issue_updated" {
|
||||
for _, change := range w.ChangeLog.Items {
|
||||
if change.Field == "resolution" {
|
||||
if change.ToString == "" && change.FromString != "" {
|
||||
verb = "reopened"
|
||||
} else if change.ToString != "" && change.FromString == "" {
|
||||
verb = "resolved"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tpl, err := template.New("post").Parse(tplBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, struct {
|
||||
*Webhook
|
||||
JIRAURL string
|
||||
Verb string
|
||||
}{
|
||||
Webhook: w,
|
||||
JIRAURL: jiraURL,
|
||||
Verb: verb,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
9
app/plugin/plugin.go
Обычный файл
9
app/plugin/plugin.go
Обычный файл
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
type Plugin interface {
|
||||
Initialize(API)
|
||||
Hooks
|
||||
}
|
||||
86
app/plugins.go
Обычный файл
86
app/plugins.go
Обычный файл
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
|
||||
"github.com/mattermost/platform/app/plugin"
|
||||
"github.com/mattermost/platform/app/plugin/jira"
|
||||
)
|
||||
|
||||
type PluginAPI struct {
|
||||
id string
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(utils.Cfg.PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *PluginAPI) PluginRouter() *mux.Router {
|
||||
return api.router
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetUserByName(name string) (*model.User, *model.AppError) {
|
||||
return GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannelByName(teamId, name string) (*model.Channel, *model.AppError) {
|
||||
return GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreatePost(post *model.Post, teamId string) (*model.Post, *model.AppError) {
|
||||
return CreatePost(post, teamId, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) I18n(id string, r *http.Request) string {
|
||||
if r != nil {
|
||||
f, _ := utils.GetTranslationsAndLocale(nil, r)
|
||||
return f(id)
|
||||
}
|
||||
f, _ := utils.GetTranslationsBySystemLocale()
|
||||
return f(id)
|
||||
}
|
||||
|
||||
func InitPlugins() {
|
||||
plugins := map[string]plugin.Plugin{
|
||||
"jira": &jira.Plugin{},
|
||||
}
|
||||
for id, p := range plugins {
|
||||
l4g.Info("Initializing plugin: " + id)
|
||||
api := &PluginAPI{
|
||||
id: id,
|
||||
router: Srv.Router.PathPrefix("/plugins/" + id).Subrouter(),
|
||||
}
|
||||
p.Initialize(api)
|
||||
}
|
||||
utils.AddConfigListener(func(before, after *model.Config) {
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
}
|
||||
})
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
}
|
||||
}
|
||||
@@ -490,48 +490,43 @@ func HandleIncomingWebhook(hookId string, req *model.IncomingWebhookRequest) *mo
|
||||
|
||||
var channel *model.Channel
|
||||
var cchan store.StoreChannel
|
||||
var directUserId string
|
||||
|
||||
if len(channelName) != 0 {
|
||||
if channelName[0] == '@' {
|
||||
if result := <-Srv.Store.User().GetByUsername(channelName[1:]); result.Err != nil {
|
||||
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message, http.StatusBadRequest)
|
||||
} else {
|
||||
directUserId = result.Data.(*model.User).Id
|
||||
channelName = model.GetDMNameFromIds(directUserId, hook.UserId)
|
||||
if ch, err := GetDirectChannel(hook.UserId, result.Data.(*model.User).Id); err != nil {
|
||||
return err
|
||||
} else {
|
||||
channel = ch
|
||||
}
|
||||
}
|
||||
} else if channelName[0] == '#' {
|
||||
channelName = channelName[1:]
|
||||
cchan = Srv.Store.Channel().GetByName(hook.TeamId, channelName[1:], true)
|
||||
} else {
|
||||
cchan = Srv.Store.Channel().GetByName(hook.TeamId, channelName, true)
|
||||
}
|
||||
|
||||
cchan = Srv.Store.Channel().GetByName(hook.TeamId, channelName, true)
|
||||
} else {
|
||||
cchan = Srv.Store.Channel().Get(hook.ChannelId, true)
|
||||
}
|
||||
|
||||
overrideUsername := req.Username
|
||||
overrideIconUrl := req.IconURL
|
||||
|
||||
result := <-cchan
|
||||
if result.Err != nil && result.Err.Id == store.MISSING_CHANNEL_ERROR && directUserId != "" {
|
||||
newChanResult := <-Srv.Store.Channel().CreateDirectChannel(directUserId, hook.UserId)
|
||||
if newChanResult.Err != nil {
|
||||
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+newChanResult.Err.Message, http.StatusBadRequest)
|
||||
if channel == nil {
|
||||
result := <-cchan
|
||||
if result.Err != nil {
|
||||
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode)
|
||||
} else {
|
||||
channel = newChanResult.Data.(*model.Channel)
|
||||
InvalidateCacheForUser(directUserId)
|
||||
InvalidateCacheForUser(hook.UserId)
|
||||
channel = result.Data.(*model.Channel)
|
||||
}
|
||||
} else if result.Err != nil {
|
||||
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode)
|
||||
} else {
|
||||
channel = result.Data.(*model.Channel)
|
||||
}
|
||||
|
||||
if channel.Type != model.CHANNEL_OPEN && !HasPermissionToChannel(hook.UserId, channel.Id, model.PERMISSION_READ_CHANNEL) {
|
||||
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
overrideUsername := req.Username
|
||||
overrideIconUrl := req.IconURL
|
||||
|
||||
if _, err := CreateWebhookPost(hook.UserId, hook.TeamId, channel.Id, text, overrideUsername, overrideIconUrl, req.Props, webhookType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user