mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
PLT-8018: Bundled jira plugin (#7920)
* bundled jira plugin * fix generated file formatting, add prepackaged key * whoops, uploaded wrong file * whitelist generated files for license check * make it work for people without go/bin in their path
This commit is contained in:
11
Makefile
11
Makefile
@@ -275,6 +275,17 @@ store-mocks:
|
||||
go get github.com/vektra/mockery/...
|
||||
GOPATH=$(shell go env GOPATH) $(shell go env GOPATH)/bin/mockery -dir store -all -output store/storetest/mocks -note 'Regenerate this file using `make store-mocks`.'
|
||||
|
||||
update-jira-plugin:
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep darwin-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_darwin_amd64.go plugin.tar.gz
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep linux-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_linux_amd64.go plugin.tar.gz
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep windows-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_windows_amd64.go plugin.tar.gz
|
||||
rm plugin.tar.gz
|
||||
gofmt -s -w ./app/plugin/jira
|
||||
|
||||
check-licenses:
|
||||
./scripts/license-check.sh $(TE_PACKAGES) $(EE_PACKAGES)
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := c.App.GetPluginManifests()
|
||||
response, err := c.App.GetPlugins()
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
||||
@@ -510,7 +510,7 @@ func (a *App) trackPlugins() {
|
||||
backendInactiveCount := 0
|
||||
settingsCount := 0
|
||||
|
||||
plugins, _ := a.GetPluginManifests()
|
||||
plugins, _ := a.GetPlugins()
|
||||
|
||||
if plugins != nil {
|
||||
totalActiveCount = len(plugins.Active)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -26,9 +27,12 @@ import (
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
)
|
||||
|
||||
var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func(string) ([]byte, error){
|
||||
"jira": jira.Asset,
|
||||
}
|
||||
|
||||
func (a *App) initBuiltInPlugins() {
|
||||
plugins := map[string]builtinplugin.Plugin{
|
||||
"jira": &jira.Plugin{},
|
||||
"ldapextras": &ldapextras.Plugin{},
|
||||
}
|
||||
for id, p := range plugins {
|
||||
@@ -106,24 +110,28 @@ func (a *App) ActivatePlugins() {
|
||||
|
||||
// InstallPlugin unpacks and installs a plugin but does not activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile, false)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model.Manifest, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
@@ -132,23 +140,27 @@ func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppEr
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if _, ok := prepackagedPlugins[manifest.Id]; ok && !allowPrepackaged {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
bundles, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest.Id == manifest.Id {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
err = utils.CopyDir(tmpPluginDir, filepath.Join(a.PluginEnv.SearchPath(), manifest.Id))
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Should add manifest validation and error handling here
|
||||
@@ -156,22 +168,26 @@ func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppEr
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (a *App) GetPluginManifests() (*model.PluginsResponse, *model.AppError) {
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPluginManifests", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
resp := &model.PluginsResponse{Active: []*model.Manifest{}, Inactive: []*model.Manifest{}}
|
||||
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
|
||||
for _, plugin := range plugins {
|
||||
info := &model.PluginInfo{
|
||||
Manifest: *plugin.Manifest,
|
||||
}
|
||||
_, info.Prepackaged = prepackagedPlugins[plugin.Manifest.Id]
|
||||
if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) {
|
||||
resp.Active = append(resp.Active, plugin.Manifest)
|
||||
resp.Active = append(resp.Active, info)
|
||||
} else {
|
||||
resp.Inactive = append(resp.Inactive, plugin.Manifest)
|
||||
resp.Inactive = append(resp.Inactive, info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,13 +210,21 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
}
|
||||
|
||||
func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return a.removePlugin(id, false)
|
||||
}
|
||||
|
||||
func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if _, ok := prepackagedPlugins[id]; ok && !allowPrepackaged {
|
||||
return model.NewAppError("removePlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
@@ -212,13 +236,13 @@ func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if a.PluginEnv.IsPluginActive(id) {
|
||||
err := a.PluginEnv.DeactivatePlugin(id)
|
||||
if err != nil {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if manifest.HasClient() {
|
||||
@@ -230,7 +254,7 @@ func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
|
||||
err = os.RemoveAll(filepath.Join(a.PluginEnv.SearchPath(), id))
|
||||
if err != nil {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -239,7 +263,7 @@ func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
// EnablePlugin will set the config for an installed plugin to enabled, triggering activation if inactive.
|
||||
func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
@@ -273,7 +297,7 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
|
||||
func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
@@ -315,19 +339,17 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
|
||||
|
||||
l4g.Info("Starting up plugins")
|
||||
|
||||
err := os.Mkdir(pluginPath, 0744)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
if err := os.Mkdir(pluginPath, 0744); err != nil && !os.IsExist(err) {
|
||||
l4g.Error("failed to start up plugins: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = os.Mkdir(webappPath, 0744)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
if err := os.Mkdir(webappPath, 0744); err != nil && !os.IsExist(err) {
|
||||
l4g.Error("failed to start up plugins: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
a.PluginEnv, err = pluginenv.New(
|
||||
if env, err := pluginenv.New(
|
||||
pluginenv.SearchPath(pluginPath),
|
||||
pluginenv.WebappPath(webappPath),
|
||||
pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
|
||||
@@ -340,11 +362,27 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
); err != nil {
|
||||
l4g.Error("failed to start up plugins: " + err.Error())
|
||||
return
|
||||
} else {
|
||||
a.PluginEnv = env
|
||||
}
|
||||
|
||||
for id, asset := range prepackagedPlugins {
|
||||
if tarball, err := asset("plugin.tar.gz"); err != nil {
|
||||
l4g.Error("failed to install prepackaged plugin: " + err.Error())
|
||||
} else if tarball != nil {
|
||||
a.removePlugin(id, true)
|
||||
if _, err := a.installPlugin(bytes.NewReader(tarball), true); err != nil {
|
||||
l4g.Error("failed to install prepackaged plugin: " + err.Error())
|
||||
}
|
||||
if _, ok := a.Config().PluginSettings.PluginStates[id]; !ok {
|
||||
if err := a.EnablePlugin(id); err != nil {
|
||||
l4g.Error("failed to enable prepackaged plugin: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,79 +1,10 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !amd64 !darwin,!linux,!windows
|
||||
|
||||
package jira
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/mattermost/mattermost-server/app/plugin"
|
||||
"github.com/mattermost/mattermost-server/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",
|
||||
"use_user_icon": "true",
|
||||
"attachments": []*model.SlackAttachment{attachment},
|
||||
},
|
||||
}); err != nil {
|
||||
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
|
||||
}
|
||||
func Asset(name string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
235
app/plugin/jira/plugin_darwin_amd64.go
Normal file
235
app/plugin/jira/plugin_darwin_amd64.go
Normal file
File diff suppressed because one or more lines are too long
235
app/plugin/jira/plugin_linux_amd64.go
Normal file
235
app/plugin/jira/plugin_linux_amd64.go
Normal file
File diff suppressed because one or more lines are too long
235
app/plugin/jira/plugin_windows_amd64.go
Normal file
235
app/plugin/jira/plugin_windows_amd64.go
Normal file
File diff suppressed because one or more lines are too long
@@ -1,174 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/mattermost/mattermost-server/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 \"\"}}" +
|
||||
"{{if len .Issue.Fields.Description | lt 3000}}" +
|
||||
"\n\n{{printf \"%.3000s\" .Issue.Fields.Description}}..." +
|
||||
"{{else}}" +
|
||||
"\n\n{{.Issue.Fields.Description}}" +
|
||||
"{{end}}" +
|
||||
"{{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) JIRAURL() string {
|
||||
pos := strings.LastIndex(w.Issue.Self, "/rest/api")
|
||||
if pos < 0 {
|
||||
return ""
|
||||
}
|
||||
return w.Issue.Self[:pos]
|
||||
}
|
||||
|
||||
func (w *Webhook) renderText(tplBody string) (string, error) {
|
||||
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: w.JIRAURL(),
|
||||
Verb: verb,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWebhookJIRAURL(t *testing.T) {
|
||||
var w Webhook
|
||||
w.Issue.Self = "http://localhost:8080/rest/api/2/issue/10006"
|
||||
assert.Equal(t, "http://localhost:8080", w.JIRAURL())
|
||||
|
||||
w.Issue.Self = "http://localhost:8080/foo/bar/rest/api/2/issue/10006"
|
||||
assert.Equal(t, "http://localhost:8080/foo/bar", w.JIRAURL())
|
||||
}
|
||||
@@ -3558,6 +3558,10 @@
|
||||
"id": "app.notification.subject.notification.full",
|
||||
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.prepackaged.app_error",
|
||||
"translation": "Prepackaged plugins cannot be modified."
|
||||
},
|
||||
{
|
||||
"id": "app.plugin.key_value.set.app_error",
|
||||
"translation": "Unable to set key value. See detailed error for more information."
|
||||
|
||||
@@ -31,9 +31,6 @@ type PluginOption struct {
|
||||
}
|
||||
|
||||
type PluginSetting struct {
|
||||
// The key that the setting will be assigned to in the configuration file.
|
||||
Key string `json:"key" yaml:"key"`
|
||||
|
||||
// The display name for the setting.
|
||||
DisplayName string `json:"display_name" yaml:"display_name"`
|
||||
|
||||
@@ -79,8 +76,8 @@ type PluginSettingsSchema struct {
|
||||
// Optional text to display below the settings.
|
||||
Footer string `json:"footer" yaml:"footer"`
|
||||
|
||||
// A list of setting definitions.
|
||||
Settings []*PluginSetting `json:"settings" yaml:"settings"`
|
||||
// A mapping of setting keys to schema definitions.
|
||||
Settings map[string]*PluginSetting `json:"settings" yaml:"settings"`
|
||||
}
|
||||
|
||||
// The plugin manifest defines the metadata required to load and present your plugin. The manifest
|
||||
@@ -96,7 +93,7 @@ type PluginSettingsSchema struct {
|
||||
// executable: myplugin
|
||||
// settings_schema:
|
||||
// settings:
|
||||
// - key: enable_extra_thing
|
||||
// enable_extra_thing:
|
||||
// type: bool
|
||||
// display_name: Enable Extra Thing
|
||||
// help_text: When true, an extra thing will be enabled!
|
||||
@@ -129,8 +126,6 @@ type Manifest struct {
|
||||
type ManifestBackend struct {
|
||||
// The path to your executable binary. This should be relative to the root of your bundle and the
|
||||
// location of the manifest file.
|
||||
//
|
||||
// On Windows, this file must have a ".exe" extension.
|
||||
Executable string `json:"executable" yaml:"executable"`
|
||||
}
|
||||
|
||||
|
||||
@@ -70,9 +70,8 @@ func TestManifestUnmarshal(t *testing.T) {
|
||||
SettingsSchema: &PluginSettingsSchema{
|
||||
Header: "theheadertext",
|
||||
Footer: "thefootertext",
|
||||
Settings: []*PluginSetting{
|
||||
&PluginSetting{
|
||||
Key: "thesetting",
|
||||
Settings: map[string]*PluginSetting{
|
||||
"thesetting": &PluginSetting{
|
||||
DisplayName: "thedisplayname",
|
||||
Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
|
||||
HelpText: "thehelptext",
|
||||
@@ -101,7 +100,7 @@ settings_schema:
|
||||
header: theheadertext
|
||||
footer: thefootertext
|
||||
settings:
|
||||
- key: thesetting
|
||||
thesetting:
|
||||
display_name: thedisplayname
|
||||
type: dropdown
|
||||
help_text: thehelptext
|
||||
@@ -126,9 +125,8 @@ settings_schema:
|
||||
"settings_schema": {
|
||||
"header": "theheadertext",
|
||||
"footer": "thefootertext",
|
||||
"settings": [
|
||||
{
|
||||
"key": "thesetting",
|
||||
"settings": {
|
||||
"thesetting": {
|
||||
"display_name": "thedisplayname",
|
||||
"type": "dropdown",
|
||||
"help_text": "thehelptext",
|
||||
@@ -142,7 +140,7 @@ settings_schema:
|
||||
],
|
||||
"default": "thedefault"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`), &jsonResult))
|
||||
assert.Equal(t, expected, jsonResult)
|
||||
@@ -177,9 +175,8 @@ func TestManifestJson(t *testing.T) {
|
||||
SettingsSchema: &PluginSettingsSchema{
|
||||
Header: "theheadertext",
|
||||
Footer: "thefootertext",
|
||||
Settings: []*PluginSetting{
|
||||
&PluginSetting{
|
||||
Key: "thesetting",
|
||||
Settings: map[string]*PluginSetting{
|
||||
"thesetting": &PluginSetting{
|
||||
DisplayName: "thedisplayname",
|
||||
Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
|
||||
HelpText: "thehelptext",
|
||||
@@ -242,9 +239,8 @@ func TestManifestClientManifest(t *testing.T) {
|
||||
SettingsSchema: &PluginSettingsSchema{
|
||||
Header: "theheadertext",
|
||||
Footer: "thefootertext",
|
||||
Settings: []*PluginSetting{
|
||||
&PluginSetting{
|
||||
Key: "thesetting",
|
||||
Settings: map[string]*PluginSetting{
|
||||
"thesetting": &PluginSetting{
|
||||
DisplayName: "thedisplayname",
|
||||
Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
|
||||
HelpText: "thehelptext",
|
||||
|
||||
@@ -8,9 +8,14 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type PluginInfo struct {
|
||||
Manifest
|
||||
Prepackaged bool `json:"prepackaged"`
|
||||
}
|
||||
|
||||
type PluginsResponse struct {
|
||||
Active []*Manifest `json:"active"`
|
||||
Inactive []*Manifest `json:"inactive"`
|
||||
Active []*PluginInfo `json:"active"`
|
||||
Inactive []*PluginInfo `json:"inactive"`
|
||||
}
|
||||
|
||||
func (m *PluginsResponse) ToJson() string {
|
||||
|
||||
@@ -19,8 +19,8 @@ func TestPluginsResponseJson(t *testing.T) {
|
||||
}
|
||||
|
||||
response := &PluginsResponse{
|
||||
Active: []*Manifest{manifest},
|
||||
Inactive: []*Manifest{},
|
||||
Active: []*PluginInfo{{Manifest: *manifest}},
|
||||
Inactive: []*PluginInfo{},
|
||||
}
|
||||
|
||||
json := response.ToJson()
|
||||
|
||||
@@ -5,7 +5,7 @@ count=0
|
||||
for fileType in GoFiles; do
|
||||
for file in `go list -f $'{{range .GoFiles}}{{$.Dir}}/{{.}}\n{{end}}' "$@"`; do
|
||||
case $file in
|
||||
*/utils/lru.go|*/store/storetest/mocks/*)
|
||||
*/utils/lru.go|*/store/storetest/mocks/*|*/app/plugin/jira/plugin_*)
|
||||
# Third-party, doesn't require a header.
|
||||
;;
|
||||
*)
|
||||
|
||||
Reference in New Issue
Block a user