mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08: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:
@@ -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)
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+68
-30
@@ -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
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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."
|
||||
|
||||
+7
-12
@@ -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,11 +93,11 @@ type PluginSettingsSchema struct {
|
||||
// executable: myplugin
|
||||
// settings_schema:
|
||||
// settings:
|
||||
// - key: enable_extra_thing
|
||||
// type: bool
|
||||
// display_name: Enable Extra Thing
|
||||
// help_text: When true, an extra thing will be enabled!
|
||||
// default: false
|
||||
// enable_extra_thing:
|
||||
// type: bool
|
||||
// display_name: Enable Extra Thing
|
||||
// help_text: When true, an extra thing will be enabled!
|
||||
// default: false
|
||||
type Manifest struct {
|
||||
// The id is a globally unique identifier that represents your plugin. Reverse-DNS notation
|
||||
// using a name you control is a good option. For example, "com.mycompany.myplugin".
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
+32
-36
@@ -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,16 +100,16 @@ settings_schema:
|
||||
header: theheadertext
|
||||
footer: thefootertext
|
||||
settings:
|
||||
- key: thesetting
|
||||
display_name: thedisplayname
|
||||
type: dropdown
|
||||
help_text: thehelptext
|
||||
regenerate_help_text: theregeneratehelptext
|
||||
placeholder: theplaceholder
|
||||
options:
|
||||
- display_name: theoptiondisplayname
|
||||
value: thevalue
|
||||
default: thedefault
|
||||
thesetting:
|
||||
display_name: thedisplayname
|
||||
type: dropdown
|
||||
help_text: thehelptext
|
||||
regenerate_help_text: theregeneratehelptext
|
||||
placeholder: theplaceholder
|
||||
options:
|
||||
- display_name: theoptiondisplayname
|
||||
value: thevalue
|
||||
default: thedefault
|
||||
`), &yamlResult))
|
||||
assert.Equal(t, expected, yamlResult)
|
||||
|
||||
@@ -126,23 +125,22 @@ settings_schema:
|
||||
"settings_schema": {
|
||||
"header": "theheadertext",
|
||||
"footer": "thefootertext",
|
||||
"settings": [
|
||||
{
|
||||
"key": "thesetting",
|
||||
"display_name": "thedisplayname",
|
||||
"type": "dropdown",
|
||||
"help_text": "thehelptext",
|
||||
"regenerate_help_text": "theregeneratehelptext",
|
||||
"placeholder": "theplaceholder",
|
||||
"options": [
|
||||
{
|
||||
"display_name": "theoptiondisplayname",
|
||||
"value": "thevalue"
|
||||
}
|
||||
],
|
||||
"default": "thedefault"
|
||||
}
|
||||
]
|
||||
"settings": {
|
||||
"thesetting": {
|
||||
"display_name": "thedisplayname",
|
||||
"type": "dropdown",
|
||||
"help_text": "thehelptext",
|
||||
"regenerate_help_text": "theregeneratehelptext",
|
||||
"placeholder": "theplaceholder",
|
||||
"options": [
|
||||
{
|
||||
"display_name": "theoptiondisplayname",
|
||||
"value": "thevalue"
|
||||
}
|
||||
],
|
||||
"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