Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MM-17818 Adding API for adding/replacing links. #85

Merged
merged 7 commits into from
Feb 3, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/mattermost/mattermost-plugin-autolink
go 1.12

require (
github.com/gorilla/mux v1.7.3
levb marked this conversation as resolved.
Show resolved Hide resolved
github.com/mattermost/mattermost-server/v5 v5.18.0
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.4.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down Expand Up @@ -120,6 +121,7 @@ github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORR
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
Expand Down
129 changes: 129 additions & 0 deletions server/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package api

import (
"context"
"encoding/json"
"fmt"
"net/http"

"github.com/gorilla/mux"
"github.com/mattermost/mattermost-plugin-autolink/server/link"
)

const PluginIDContextValue = "pluginid"
levb marked this conversation as resolved.
Show resolved Hide resolved

type LinkStore interface {
cpoile marked this conversation as resolved.
Show resolved Hide resolved
crspeller marked this conversation as resolved.
Show resolved Hide resolved
GetLinks() []link.Link
SaveLinks([]link.Link) error
}

type Authorization interface {
IsAuthorizedAdmin(userID string) (bool, error)
}

type Handler struct {
root *mux.Router
linkStore LinkStore
authorization Authorization
}

func NewHandler(linkStore LinkStore, authorization Authorization) *Handler {
h := &Handler{
linkStore: linkStore,
authorization: authorization,
}

root := mux.NewRouter()
api := root.PathPrefix("/api/v1").Subrouter()
api.Use(h.adminOrPluginRequired)
cpoile marked this conversation as resolved.
Show resolved Hide resolved
api.HandleFunc("/link", h.setLink).Methods("POST")

api.Handle("{anything:.*}", http.NotFoundHandler())

h.root = root

return h
}

func (h *Handler) handleError(w http.ResponseWriter, err error) {
crspeller marked this conversation as resolved.
Show resolved Hide resolved
w.WriteHeader(http.StatusInternalServerError)
b, _ := json.Marshal(struct {
Error string `json:"error"`
Details string `json:"details"`
}{
Error: "An internal error has occurred. Check app server logs for details.",
Details: err.Error(),
})
_, _ = w.Write(b)
}

func (h *Handler) handleErrorWithCode(w http.ResponseWriter, code int, errTitle string, err error) {
w.WriteHeader(code)
crspeller marked this conversation as resolved.
Show resolved Hide resolved
b, _ := json.Marshal(struct {
Error string `json:"error"`
Details string `json:"details"`
}{
Error: errTitle,
Details: err.Error(),
})
_, _ = w.Write(b)
}

func (h *Handler) adminOrPluginRequired(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID != "" {
authorized, err := h.authorization.IsAuthorizedAdmin(userID)
if err != nil {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
if authorized {
next.ServeHTTP(w, r)
return
}
}

ifPluginId := r.Context().Value(PluginIDContextValue)
pluginId, ok := ifPluginId.(string)
if ok && pluginId != "" {
next.ServeHTTP(w, r)
return
}

http.Error(w, "Not authorized", http.StatusUnauthorized)
cpoile marked this conversation as resolved.
Show resolved Hide resolved
})
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, fromPluginID string) {
h.root.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), PluginIDContextValue, fromPluginID)))
}

func (h *Handler) setLink(w http.ResponseWriter, r *http.Request) {
var newLink link.Link
if err := json.NewDecoder(r.Body).Decode(&newLink); err != nil {
h.handleError(w, fmt.Errorf("Unable to decode body: %w", err))
return
}

links := h.linkStore.GetLinks()
found := false
for i := range links {
if links[i].Name == newLink.Name || links[i].Pattern == newLink.Pattern {
links[i] = newLink
found = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Was wondering how/where the uniqueness is checked

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note, @crspeller we should not be saving what hasn't changed. I think it's just too wasteful, TBH. Add (content) de=duplication, maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added #88

break
}
}
if !found {
links = append(h.linkStore.GetLinks(), newLink)
}

if err := h.linkStore.SaveLinks(links); err != nil {
h.handleError(w, fmt.Errorf("Unable to save link: %w", err))
return
}

w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status": "OK"}`))
crspeller marked this conversation as resolved.
Show resolved Hide resolved
}
9 changes: 5 additions & 4 deletions server/command.go → server/autolinkplugin/command.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package main
package autolinkplugin

import (
"fmt"
"strconv"
"strings"

"github.com/mattermost/mattermost-plugin-autolink/server/link"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
"github.com/pkg/errors"
Expand Down Expand Up @@ -261,7 +262,7 @@ func executeAdd(p *Plugin, c *plugin.Context, header *model.CommandArgs, args ..
name = args[0]
}

err := saveConfigLinks(p, append(p.getConfig().Links, Link{
err := saveConfigLinks(p, append(p.getConfig().Links, link.Link{
Name: name,
}))
if err != nil {
Expand All @@ -286,7 +287,7 @@ func responsef(format string, args ...interface{}) *model.CommandResponse {
}
}

func parseLinkRef(p *Plugin, requireUnique bool, args ...string) ([]Link, []int, error) {
func parseLinkRef(p *Plugin, requireUnique bool, args ...string) ([]link.Link, []int, error) {
links := p.getConfig().Sorted().Links
if len(args) == 0 {
if requireUnique {
Expand Down Expand Up @@ -334,7 +335,7 @@ func parseBoolArg(arg string) (bool, error) {
return false, errors.Errorf("Not a bool, %q", arg)
}

func saveConfigLinks(p *Plugin, links []Link) error {
func saveConfigLinks(p *Plugin, links []link.Link) error {
conf := p.getConfig()
conf.Links = links
appErr := p.API.SavePluginConfig(conf.ToConfig())
Expand Down
29 changes: 24 additions & 5 deletions server/config.go → server/autolinkplugin/config.go
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package main
package autolinkplugin

import (
"fmt"
"sort"
"strings"

"github.com/mattermost/mattermost-plugin-autolink/server/link"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
)

// Config from config.json
type Config struct {
EnableAdminCommand bool
Links []Link
Links []link.Link
}

// OnConfigurationChange is invoked when configuration changes may have been made.
Expand All @@ -30,7 +31,7 @@ func (p *Plugin) OnConfigurationChange() error {
}
}

p.updateConfig(func(conf *Config) {
p.UpdateConfig(func(conf *Config) {
*conf = c
})

Expand All @@ -57,7 +58,25 @@ func (p *Plugin) getConfig() Config {
return p.conf
}

func (p *Plugin) updateConfig(f func(conf *Config)) Config {
func (p *Plugin) GetLinks() []link.Link {
p.confLock.RLock()
defer p.confLock.RUnlock()
return p.conf.Links
}

func (p *Plugin) SaveLinks(links []link.Link) error {
p.UpdateConfig(func(conf *Config) {
conf.Links = links
})
appErr := p.API.SavePluginConfig(p.getConfig().ToConfig())
if appErr != nil {
return fmt.Errorf("Unable to save links: %w", appErr)
}

return nil
}

func (p *Plugin) UpdateConfig(f func(conf *Config)) Config {
p.confLock.Lock()
defer p.confLock.Unlock()

Expand All @@ -81,7 +100,7 @@ func (conf Config) ToConfig() map[string]interface{} {
// Sorted returns a clone of the Config, with links sorted alphabetically
func (conf Config) Sorted() Config {
sorted := conf
sorted.Links = append([]Link{}, conf.Links...)
sorted.Links = append([]link.Link{}, conf.Links...)
sort.Slice(conf.Links, func(i, j int) bool {
return strings.Compare(conf.Links[i].DisplayName(), conf.Links[j].DisplayName()) < 0
})
Expand Down
49 changes: 47 additions & 2 deletions server/plugin.go → server/autolinkplugin/plugin.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package main
package autolinkplugin

import (
"fmt"
"net/http"
"strings"
"sync"

"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-plugin-autolink/server/api"

"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
"github.com/mattermost/mattermost-server/v5/utils/markdown"
Expand All @@ -15,11 +18,49 @@ import (
type Plugin struct {
plugin.MattermostPlugin

handler *api.Handler

// configuration and a muttex to control concurrent access
conf Config
confLock sync.RWMutex
}

func (p *Plugin) OnActivate() error {
p.handler = api.NewHandler(p, p)

return nil
}

func (p *Plugin) IsAuthorizedAdmin(mattermostID string) (bool, error) {
user, err := p.API.GetUser(mattermostID)
if err != nil {
return false, err
}
if strings.Contains(user.Roles, "system_admin") {
return true, nil
}
return false, nil
}

func contains(team string, channel string, list []string) bool {
levb marked this conversation as resolved.
Show resolved Hide resolved
for _, channelTeam := range list {
channelTeamSplit := strings.Split(channelTeam, "/")
if len(channelTeamSplit) == 2 {
if strings.EqualFold(channelTeamSplit[0], team) && strings.EqualFold(channelTeamSplit[1], channel) {
return true
}
} else if len(channelTeamSplit) == 1 {
if strings.EqualFold(channelTeamSplit[0], team) {
return true
}
} else {
mlog.Error("error splitting channel & team combination.")
}

}
return false
}

// MessageWillBePosted is invoked when a message is posted by a user before it is committed
// to the database.
func (p *Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
Expand Down Expand Up @@ -98,3 +139,7 @@ func (p *Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*mode

return post, ""
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
p.handler.ServeHTTP(w, r, c.SourcePluginId)
}
Loading