* 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
Этот коммит содержится в:
Chris
2017-11-30 14:55:44 -06:00
коммит произвёл GitHub
родитель eaca461ee3
Коммит daebd26a28
17 изменённых файлов: 843 добавлений и 358 удалений

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

@@ -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 Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

235
app/plugin/jira/plugin_linux_amd64.go Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

235
app/plugin/jira/plugin_windows_amd64.go Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

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