mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-11 18:13:52 +02:00
Add a plugin adapter to reuse MM websocket in plugin mode (#1079)
* Add a plugin adapter to reuse MM websocket in plugin mode * Remove development replace * Switch all go.mod files to use 1.16 * Fix linter issues * Fix linter * Update server version to contain the new hooks
This commit is contained in:
parent
c3f5993126
commit
e10229031f
@ -1,6 +1,6 @@
|
||||
module github.com/mattermost/focalboard/linux
|
||||
|
||||
go 1.15
|
||||
go 1.16
|
||||
|
||||
replace github.com/mattermost/focalboard/server => ../server
|
||||
|
||||
|
10
linux/go.sum
10
linux/go.sum
@ -230,6 +230,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
|
||||
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
@ -404,6 +405,7 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.16.1 h1:IVQwpTGNRRIHafnTs2dQLIk4ENtneRIEEJWOVDqz99o=
|
||||
github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
@ -413,6 +415,7 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0=
|
||||
github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
@ -434,6 +437,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/memberlist v0.2.4/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 h1:Y4V+SFe7d3iH+9pJCoeWIOS5/xBJIFsltS7E+KJSsJY=
|
||||
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
@ -474,6 +478,7 @@ github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvz
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
||||
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
@ -584,6 +589,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
@ -592,6 +598,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
@ -627,6 +634,7 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
@ -1211,7 +1219,6 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -1427,7 +1434,6 @@ gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -60,7 +60,7 @@ func runServer(port int) (*server.Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server, err := server.New(config, sessionToken, db, logger, "")
|
||||
server, err := server.New(config, sessionToken, db, logger, "", nil)
|
||||
if err != nil {
|
||||
fmt.Println("ERROR INITIALIZING THE SERVER", err)
|
||||
return nil, err
|
||||
|
@ -1,6 +1,6 @@
|
||||
module github.com/mattermost/focalboard/mattermost-plugin
|
||||
|
||||
go 1.12
|
||||
go 1.16
|
||||
|
||||
replace github.com/mattermost/focalboard/server => ../server
|
||||
|
||||
|
@ -1225,7 +1225,6 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -1441,7 +1440,6 @@ gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -6,15 +6,17 @@ import (
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
@ -30,30 +32,8 @@ type Plugin struct {
|
||||
// setConfiguration for usage.
|
||||
configuration *configuration
|
||||
|
||||
server *server.Server
|
||||
wsHub *WSHub
|
||||
}
|
||||
|
||||
type WSHub struct {
|
||||
API plugin.API
|
||||
handleWSMessage func(data []byte)
|
||||
}
|
||||
|
||||
func (h *WSHub) SendWSMessage(data []byte) {
|
||||
err := h.API.PublishPluginClusterEvent(model.PluginClusterEvent{
|
||||
Id: "websocket_event",
|
||||
Data: data,
|
||||
}, model.PluginClusterEventSendOptions{
|
||||
SendType: model.PluginClusterEventSendTypeReliable,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
h.API.LogError("Error sending websocket message", map[string]interface{}{"err": err})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WSHub) SetReceiveWSMessage(handler func(data []byte)) {
|
||||
h.handleWSMessage = handler
|
||||
server *server.Server
|
||||
wsPluginAdapter *ws.PluginAdapter
|
||||
}
|
||||
|
||||
func (p *Plugin) OnActivate() error {
|
||||
@ -143,23 +123,28 @@ func (p *Plugin) OnActivate() error {
|
||||
}
|
||||
|
||||
serverID := client.System.GetDiagnosticID()
|
||||
p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db))
|
||||
|
||||
server, err := server.New(cfg, "", db, logger, serverID)
|
||||
server, err := server.New(cfg, "", db, logger, serverID, p.wsPluginAdapter)
|
||||
if err != nil {
|
||||
fmt.Println("ERROR INITIALIZING THE SERVER", err)
|
||||
return err
|
||||
}
|
||||
|
||||
p.wsHub = &WSHub{API: p.API}
|
||||
server.SetWSHub(p.wsHub)
|
||||
p.server = server
|
||||
return server.Start()
|
||||
}
|
||||
|
||||
func (p *Plugin) OnPluginClusterEvent(_ *plugin.Context, ev model.PluginClusterEvent) {
|
||||
if ev.Id == "websocket_event" {
|
||||
p.wsHub.handleWSMessage(ev.Data)
|
||||
}
|
||||
func (p *Plugin) OnWebSocketConnect(webConnID, userID string) {
|
||||
p.wsPluginAdapter.OnWebSocketConnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
p.wsPluginAdapter.OnWebSocketDisconnect(webConnID, userID)
|
||||
}
|
||||
|
||||
func (p *Plugin) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mmModel.WebSocketRequest) {
|
||||
p.wsPluginAdapter.WebSocketMessageHasBeenPosted(webConnID, userID, req)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnDeactivate() error {
|
||||
|
@ -18,6 +18,7 @@ import store from '../../../webapp/src/store'
|
||||
import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader'
|
||||
import FocalboardIcon from '../../../webapp/src/widgets/icons/logo'
|
||||
import {setMattermostTheme} from '../../../webapp/src/theme'
|
||||
import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK} from './../../../webapp/src/wsclient'
|
||||
|
||||
import '../../../webapp/src/styles/focalboard-variables.scss'
|
||||
import '../../../webapp/src/styles/main.scss'
|
||||
@ -31,8 +32,13 @@ import {PluginRegistry} from './types/mattermost-webapp'
|
||||
|
||||
import './plugin.scss'
|
||||
|
||||
const MainApp = () => {
|
||||
type Props = {
|
||||
webSocketClient: MMWebSocketClient
|
||||
}
|
||||
|
||||
const MainApp = (props: Props) => {
|
||||
const [faviconStored, setFaviconStored] = useState(false)
|
||||
wsClient.initPlugin(manifest.id, props.webSocketClient)
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add('focalboard-body')
|
||||
@ -143,12 +149,18 @@ export default class Plugin {
|
||||
}, '', 'Boards')
|
||||
this.registry.registerCustomRoute('/', MainApp)
|
||||
}
|
||||
|
||||
// register websocket handlers
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK}`, (e: any) => wsClient.updateBlockHandler(e.data))
|
||||
}
|
||||
|
||||
uninitialize(): void {
|
||||
if (this.channelHeaderButtonId) {
|
||||
this.registry?.unregisterComponent(this.channelHeaderButtonId)
|
||||
}
|
||||
|
||||
// unregister websocket handlers
|
||||
this.registry?.unregisterWebSocketEventHandler(wsClient.clientPrefix + ACTION_UPDATE_BLOCK)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ export interface PluginRegistry {
|
||||
registerProductRoute(route: string, component: React.ElementType)
|
||||
unregisterComponent(componentId: string)
|
||||
registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCompoent: React.ElementType)
|
||||
registerWebSocketEventHandler(event: string, handler: (e: any) => void)
|
||||
unregisterWebSocketEventHandler(event: string)
|
||||
|
||||
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
|
||||
}
|
||||
|
@ -2,22 +2,17 @@ package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/metrics"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/filestore"
|
||||
)
|
||||
|
||||
type WebsocketServer interface {
|
||||
BroadcastBlockChange(workspaceID string, block model.Block)
|
||||
BroadcastBlockDelete(workspaceID, blockID, parentID string)
|
||||
}
|
||||
|
||||
type Services struct {
|
||||
Auth *auth.Auth
|
||||
Store store.Store
|
||||
@ -31,19 +26,19 @@ type App struct {
|
||||
config *config.Configuration
|
||||
store store.Store
|
||||
auth *auth.Auth
|
||||
wsServer WebsocketServer
|
||||
wsAdapter ws.Adapter
|
||||
filesBackend filestore.FileBackend
|
||||
webhook *webhook.Client
|
||||
metrics *metrics.Metrics
|
||||
logger *mlog.Logger
|
||||
}
|
||||
|
||||
func New(config *config.Configuration, wsServer WebsocketServer, services Services) *App {
|
||||
func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App {
|
||||
return &App{
|
||||
config: config,
|
||||
store: services.Store,
|
||||
auth: services.Auth,
|
||||
wsServer: wsServer,
|
||||
wsAdapter: wsAdapter,
|
||||
filesBackend: services.FilesBackend,
|
||||
webhook: services.Webhook,
|
||||
metrics: services.Metrics,
|
||||
|
@ -39,7 +39,7 @@ func (a *App) PatchBlock(c store.Container, blockID string, blockPatch *model.Bl
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
a.wsServer.BroadcastBlockChange(c.WorkspaceID, *block)
|
||||
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block)
|
||||
go a.webhook.NotifyUpdate(*block)
|
||||
return nil
|
||||
}
|
||||
@ -59,7 +59,7 @@ func (a *App) InsertBlocks(c store.Container, blocks []model.Block, userID strin
|
||||
return err
|
||||
}
|
||||
|
||||
a.wsServer.BroadcastBlockChange(c.WorkspaceID, blocks[i])
|
||||
a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, blocks[i])
|
||||
a.metrics.IncrementBlocksInserted(len(blocks))
|
||||
go a.webhook.NotifyUpdate(blocks[i])
|
||||
}
|
||||
@ -90,7 +90,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string)
|
||||
return err
|
||||
}
|
||||
|
||||
a.wsServer.BroadcastBlockDelete(c.WorkspaceID, blockID, parentID)
|
||||
a.wsAdapter.BroadcastBlockDelete(c.WorkspaceID, blockID, parentID)
|
||||
a.metrics.IncrementBlocksDeleted(1)
|
||||
|
||||
return nil
|
||||
|
@ -579,7 +579,6 @@ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ=
|
||||
github.com/mattermost/logr v1.0.13 h1:6F/fM3csvH6Oy5sUpJuW7YyZSzZZAhJm5VcgKMxA2P8=
|
||||
github.com/mattermost/logr v1.0.13/go.mod h1:Mt4DPu1NXMe6JxPdwCC0XBoxXmN9eXOIRPoZarU2PXs=
|
||||
github.com/mattermost/logr/v2 v2.0.10 h1:i6rJbuX/EkBM9maM8M0eJ3rxB+fsBKNslPvzSlA2w/M=
|
||||
github.com/mattermost/logr/v2 v2.0.10/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg=
|
||||
github.com/mattermost/logr/v2 v2.0.11 h1:eGg73t/HHkirGq34S+r6geGVuuVVrHDbW26QXCog6aw=
|
||||
github.com/mattermost/logr/v2 v2.0.11/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg=
|
||||
|
@ -75,7 +75,7 @@ func newTestServer(singleUserToken string) *server.Server {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
srv, err := server.New(cfg, singleUserToken, db, logger, "")
|
||||
srv, err := server.New(cfg, singleUserToken, db, logger, "", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -165,7 +165,7 @@ func main() {
|
||||
logger.Fatal("server.NewStore ERROR", mlog.Err(err))
|
||||
}
|
||||
|
||||
server, err := server.New(config, singleUserToken, db, logger, "")
|
||||
server, err := server.New(config, singleUserToken, db, logger, "", nil)
|
||||
if err != nil {
|
||||
logger.Fatal("server.New ERROR", mlog.Err(err))
|
||||
}
|
||||
@ -245,7 +245,7 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db
|
||||
logger.Fatal("server.NewStore ERROR", mlog.Err(err))
|
||||
}
|
||||
|
||||
pServer, err = server.New(config, singleUserToken, db, logger, "")
|
||||
pServer, err = server.New(config, singleUserToken, db, logger, "", nil)
|
||||
if err != nil {
|
||||
logger.Fatal("server.New ERROR", mlog.Err(err))
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ const (
|
||||
|
||||
type Server struct {
|
||||
config *config.Configuration
|
||||
wsServer *ws.Server
|
||||
wsAdapter ws.Adapter
|
||||
webServer *web.Server
|
||||
store store.Store
|
||||
filesBackend filestore.FileBackend
|
||||
@ -67,10 +67,14 @@ type Server struct {
|
||||
api *api.API
|
||||
}
|
||||
|
||||
func New(cfg *config.Configuration, singleUserToken string, db store.Store, logger *mlog.Logger, serverID string) (*Server, error) {
|
||||
func New(cfg *config.Configuration, singleUserToken string, db store.Store,
|
||||
logger *mlog.Logger, serverID string, wsAdapter ws.Adapter) (*Server, error) {
|
||||
authenticator := auth.New(cfg, db)
|
||||
|
||||
wsServer := ws.NewServer(authenticator, singleUserToken, cfg.AuthMode == MattermostAuthMod, logger)
|
||||
// if no ws adapter is provided, we spin up a websocket server
|
||||
if wsAdapter == nil {
|
||||
wsAdapter = ws.NewServer(authenticator, singleUserToken, cfg.AuthMode == MattermostAuthMod, logger)
|
||||
}
|
||||
|
||||
filesBackendSettings := filestore.FileBackendSettings{}
|
||||
filesBackendSettings.DriverName = cfg.FilesDriver
|
||||
@ -121,7 +125,7 @@ func New(cfg *config.Configuration, singleUserToken string, db store.Store, logg
|
||||
Metrics: metricsService,
|
||||
Logger: logger,
|
||||
}
|
||||
app := app.New(cfg, wsServer, appServices)
|
||||
app := app.New(cfg, wsAdapter, appServices)
|
||||
|
||||
focalboardAPI := api.NewAPI(app, singleUserToken, cfg.AuthMode, logger, auditService)
|
||||
|
||||
@ -136,7 +140,10 @@ func New(cfg *config.Configuration, singleUserToken string, db store.Store, logg
|
||||
}
|
||||
|
||||
webServer := web.NewServer(cfg.WebPath, cfg.ServerRoot, cfg.Port, cfg.UseSSL, cfg.LocalOnly, logger)
|
||||
webServer.AddRoutes(wsServer)
|
||||
// if the adapter is a routed service, register it before the API
|
||||
if routedService, ok := wsAdapter.(web.RoutedService); ok {
|
||||
webServer.AddRoutes(routedService)
|
||||
}
|
||||
webServer.AddRoutes(focalboardAPI)
|
||||
|
||||
settings, err := db.GetSystemSettings()
|
||||
@ -164,7 +171,7 @@ func New(cfg *config.Configuration, singleUserToken string, db store.Store, logg
|
||||
|
||||
server := Server{
|
||||
config: cfg,
|
||||
wsServer: wsServer,
|
||||
wsAdapter: wsAdapter,
|
||||
webServer: webServer,
|
||||
store: db,
|
||||
filesBackend: filesBackend,
|
||||
@ -364,10 +371,6 @@ func (s *Server) GetRootRouter() *mux.Router {
|
||||
return s.webServer.Router()
|
||||
}
|
||||
|
||||
func (s *Server) SetWSHub(hub ws.Hub) {
|
||||
s.wsServer.SetHub(hub)
|
||||
}
|
||||
|
||||
type telemetryOptions struct {
|
||||
app *app.App
|
||||
cfg *config.Configuration
|
||||
|
19
server/ws/adapter.go
Normal file
19
server/ws/adapter.go
Normal file
@ -0,0 +1,19 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
)
|
||||
|
||||
const (
|
||||
websocketActionAuth = "AUTH"
|
||||
websocketActionSubscribeWorkspace = "SUBSCRIBE_WORKSPACE"
|
||||
websocketActionUnsubscribeWorkspace = "UNSUBSCRIBE_WORKSPACE"
|
||||
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
||||
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
||||
websocketActionUpdateBlock = "UPDATE_BLOCK"
|
||||
)
|
||||
|
||||
type Adapter interface {
|
||||
BroadcastBlockChange(workspaceID string, block model.Block)
|
||||
BroadcastBlockDelete(workspaceID, blockID, parentID string)
|
||||
}
|
314
server/ws/plugin_adapter.go
Normal file
314
server/ws/plugin_adapter.go
Normal file
@ -0,0 +1,314 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
)
|
||||
|
||||
const websocketMessagePrefix = "custom_focalboard_"
|
||||
|
||||
var errMissingWorkspaceInCommand = fmt.Errorf("command doesn't contain workspaceId")
|
||||
|
||||
func structToMap(v interface{}) (m map[string]interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
_ = json.Unmarshal(b, &m)
|
||||
return
|
||||
}
|
||||
|
||||
type PluginAdapterClient struct {
|
||||
webConnID string
|
||||
userID string
|
||||
workspaces []string
|
||||
blocks []string
|
||||
}
|
||||
|
||||
func (pac *PluginAdapterClient) isSubscribedToWorkspace(workspaceID string) bool {
|
||||
for _, id := range pac.workspaces {
|
||||
if id == workspaceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (pac *PluginAdapterClient) isSubscribedToBlock(blockID string) bool {
|
||||
for _, id := range pac.blocks {
|
||||
if id == blockID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type PluginAdapter struct {
|
||||
api plugin.API
|
||||
auth *auth.Auth
|
||||
|
||||
listeners map[string]*PluginAdapterClient
|
||||
listenersByWorkspace map[string][]*PluginAdapterClient
|
||||
listenersByBlock map[string][]*PluginAdapterClient
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewPluginAdapter(api plugin.API, auth *auth.Auth) *PluginAdapter {
|
||||
return &PluginAdapter{
|
||||
api: api,
|
||||
auth: auth,
|
||||
listeners: make(map[string]*PluginAdapterClient),
|
||||
listenersByWorkspace: make(map[string][]*PluginAdapterClient),
|
||||
listenersByBlock: make(map[string][]*PluginAdapterClient),
|
||||
mu: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) addListener(pac *PluginAdapterClient) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
pa.listeners[pac.webConnID] = pac
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) removeListener(pac *PluginAdapterClient) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
// workspace subscriptions
|
||||
for _, workspace := range pac.workspaces {
|
||||
pa.removeListenerFromWorkspace(pac, workspace)
|
||||
}
|
||||
|
||||
// block subscriptions
|
||||
for _, block := range pac.blocks {
|
||||
pa.removeListenerFromBlock(pac, block)
|
||||
}
|
||||
|
||||
delete(pa.listeners, pac.webConnID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) removeListenerFromWorkspace(pac *PluginAdapterClient, workspaceID string) {
|
||||
newWorkspaceListeners := []*PluginAdapterClient{}
|
||||
for _, listener := range pa.listenersByWorkspace[workspaceID] {
|
||||
if listener.webConnID != pac.webConnID {
|
||||
newWorkspaceListeners = append(newWorkspaceListeners, listener)
|
||||
}
|
||||
}
|
||||
pa.listenersByWorkspace[workspaceID] = newWorkspaceListeners
|
||||
|
||||
newClientWorkspaces := []string{}
|
||||
for _, id := range pac.workspaces {
|
||||
if id != workspaceID {
|
||||
newClientWorkspaces = append(newClientWorkspaces, id)
|
||||
}
|
||||
}
|
||||
pac.workspaces = newClientWorkspaces
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) removeListenerFromBlock(pac *PluginAdapterClient, blockID string) {
|
||||
newBlockListeners := []*PluginAdapterClient{}
|
||||
for _, listener := range pa.listenersByBlock[blockID] {
|
||||
if listener.webConnID != pac.webConnID {
|
||||
newBlockListeners = append(newBlockListeners, listener)
|
||||
}
|
||||
}
|
||||
pa.listenersByBlock[blockID] = newBlockListeners
|
||||
|
||||
newClientBlocks := []string{}
|
||||
for _, id := range pac.blocks {
|
||||
if id != blockID {
|
||||
newClientBlocks = append(newClientBlocks, id)
|
||||
}
|
||||
}
|
||||
pac.blocks = newClientBlocks
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) subscribeListenerToWorkspace(pac *PluginAdapterClient, workspaceID string) {
|
||||
if pac.isSubscribedToWorkspace(workspaceID) {
|
||||
return
|
||||
}
|
||||
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
pa.listenersByWorkspace[workspaceID] = append(pa.listenersByWorkspace[workspaceID], pac)
|
||||
pac.workspaces = append(pac.workspaces, workspaceID)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) unsubscribeListenerFromWorkspace(pac *PluginAdapterClient, workspaceID string) {
|
||||
if !pac.isSubscribedToWorkspace(workspaceID) {
|
||||
return
|
||||
}
|
||||
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
pa.removeListenerFromWorkspace(pac, workspaceID)
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (pa *PluginAdapter) unsubscribeListenerFromBlocks(pac *PluginAdapterClient, blockIDs []string) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
|
||||
for _, blockID := range blockIDs {
|
||||
if pac.isSubscribedToBlock(blockID) {
|
||||
pa.removeListenerFromBlock(pac, blockID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) OnWebSocketConnect(webConnID, userID string) {
|
||||
pac := &PluginAdapterClient{
|
||||
webConnID: webConnID,
|
||||
userID: userID,
|
||||
workspaces: []string{},
|
||||
blocks: []string{},
|
||||
}
|
||||
|
||||
pa.addListener(pac)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) OnWebSocketDisconnect(webConnID, userID string) {
|
||||
pac, ok := pa.listeners[webConnID]
|
||||
if !ok {
|
||||
pa.api.LogError("received a disconnect for an unregistered webconn",
|
||||
"webConnID", webConnID,
|
||||
"userID", userID,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
pa.removeListener(pac)
|
||||
}
|
||||
|
||||
func commandFromRequest(req *mmModel.WebSocketRequest) (*WebsocketCommand, error) {
|
||||
c := &WebsocketCommand{Action: strings.TrimPrefix(req.Action, websocketMessagePrefix)}
|
||||
|
||||
if workspaceID, ok := req.Data["workspaceId"]; ok {
|
||||
c.WorkspaceID = workspaceID.(string)
|
||||
} else {
|
||||
return nil, errMissingWorkspaceInCommand
|
||||
}
|
||||
|
||||
if readToken, ok := req.Data["readToken"]; ok {
|
||||
c.ReadToken = readToken.(string)
|
||||
}
|
||||
|
||||
if blockIDs, ok := req.Data["blockIds"]; ok {
|
||||
c.BlockIDs = blockIDs.([]string)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) WebSocketMessageHasBeenPosted(webConnID, userID string, req *mmModel.WebSocketRequest) {
|
||||
pac, ok := pa.listeners[webConnID]
|
||||
if !ok {
|
||||
pa.api.LogError("received a message for an unregistered webconn",
|
||||
"webConnID", webConnID,
|
||||
"userID", userID,
|
||||
"action", req.Action,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// only process messages using the plugin actions
|
||||
if !strings.HasPrefix(req.Action, websocketMessagePrefix) {
|
||||
return
|
||||
}
|
||||
|
||||
command, err := commandFromRequest(req)
|
||||
if err != nil {
|
||||
pa.api.LogError("error getting command from request",
|
||||
"err", err,
|
||||
"action", req.Action,
|
||||
"webConnID", webConnID,
|
||||
"userID", userID,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switch command.Action {
|
||||
// The block-related commands are not implemented in the adapter
|
||||
// as there is no such thing as unauthenticated websocket
|
||||
// connections in plugin mode. Only a debug line is logged
|
||||
case websocketActionSubscribeBlocks, websocketActionUnsubscribeBlocks:
|
||||
pa.api.LogDebug(`Command not implemented in plugin mode`,
|
||||
"command", command.Action,
|
||||
"webConnID", webConnID,
|
||||
"userID", userID,
|
||||
"workspaceID", command.WorkspaceID,
|
||||
)
|
||||
|
||||
case websocketActionSubscribeWorkspace:
|
||||
pa.api.LogDebug(`Command: SUBSCRIBE_WORKSPACE`,
|
||||
"webConnID", webConnID,
|
||||
"userID", userID,
|
||||
"workspaceID", command.WorkspaceID,
|
||||
)
|
||||
|
||||
if !pa.auth.DoesUserHaveWorkspaceAccess(userID, command.WorkspaceID) {
|
||||
return
|
||||
}
|
||||
|
||||
pa.subscribeListenerToWorkspace(pac, command.WorkspaceID)
|
||||
case websocketActionUnsubscribeWorkspace:
|
||||
pa.api.LogDebug(`Command: UNSUBSCRIBE_WORKSPACE`,
|
||||
"webConnID", webConnID,
|
||||
"userID", userID,
|
||||
"workspaceID", command.WorkspaceID,
|
||||
)
|
||||
|
||||
pa.unsubscribeListenerFromWorkspace(pac, command.WorkspaceID)
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) getUserIDsForWorkspace(workspaceID string) []string {
|
||||
userMap := map[string]bool{}
|
||||
for _, pac := range pa.listenersByWorkspace[workspaceID] {
|
||||
userMap[pac.userID] = true
|
||||
}
|
||||
|
||||
userIDs := []string{}
|
||||
for userID := range userMap {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBlockChange(workspaceID string, block model.Block) {
|
||||
pa.api.LogInfo("BroadcastingBlockChange",
|
||||
"workspaceID", workspaceID,
|
||||
"blockID", block.ID,
|
||||
)
|
||||
|
||||
message := UpdateMsg{
|
||||
Action: websocketActionUpdateBlock,
|
||||
Block: block,
|
||||
}
|
||||
|
||||
userIDs := pa.getUserIDsForWorkspace(workspaceID)
|
||||
for _, userID := range userIDs {
|
||||
pa.api.PublishWebSocketEvent(websocketActionUpdateBlock, structToMap(message), &mmModel.WebsocketBroadcast{UserId: userID})
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBlockDelete(workspaceID, blockID, parentID string) {
|
||||
now := time.Now().Unix()
|
||||
block := model.Block{}
|
||||
block.ID = blockID
|
||||
block.ParentID = parentID
|
||||
block.UpdateAt = now
|
||||
block.DeleteAt = now
|
||||
|
||||
pa.BroadcastBlockChange(workspaceID, block)
|
||||
}
|
@ -2,7 +2,6 @@ package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@ -16,30 +15,18 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
singleUserID = "single-user-id"
|
||||
websocketActionAuth = "AUTH"
|
||||
websocketActionSubscribeWorkspace = "SUBSCRIBE_WORKSPACE"
|
||||
websocketActionUnsubscribeWorkspace = "UNSUBSCRIBE_WORKSPACE"
|
||||
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
||||
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
||||
)
|
||||
|
||||
type Hub interface {
|
||||
SendWSMessage(data []byte)
|
||||
SetReceiveWSMessage(func(data []byte))
|
||||
}
|
||||
const singleUserID = "single-user-id"
|
||||
|
||||
type wsClient struct {
|
||||
*websocket.Conn
|
||||
lock *sync.Mutex
|
||||
mu sync.Mutex
|
||||
workspaces []string
|
||||
blocks []string
|
||||
}
|
||||
|
||||
func (c *wsClient) WriteJSON(v interface{}) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
err := c.Conn.WriteJSON(v)
|
||||
return err
|
||||
}
|
||||
@ -72,7 +59,6 @@ type Server struct {
|
||||
listenersByBlock map[string][]*wsClient
|
||||
mu sync.RWMutex
|
||||
auth *auth.Auth
|
||||
hub Hub
|
||||
singleUserToken string
|
||||
isMattermostAuth bool
|
||||
logger *mlog.Logger
|
||||
@ -84,13 +70,6 @@ type UpdateMsg struct {
|
||||
Block model.Block `json:"block"`
|
||||
}
|
||||
|
||||
// clusterUpdateMsg is sent on block updates.
|
||||
type clusterUpdateMsg struct {
|
||||
UpdateMsg
|
||||
BlockID string `json:"block_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
}
|
||||
|
||||
// WebsocketCommand is an incoming command from the client.
|
||||
type WebsocketCommand struct {
|
||||
Action string `json:"action"`
|
||||
@ -142,7 +121,7 @@ func (ws *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// create an empty session with websocket client
|
||||
wsSession := websocketSession{
|
||||
client: &wsClient{client, &sync.Mutex{}, []string{}, []string{}},
|
||||
client: &wsClient{client, sync.Mutex{}, []string{}, []string{}},
|
||||
userID: "",
|
||||
}
|
||||
|
||||
@ -454,6 +433,10 @@ func (ws *Server) getUserIDForToken(token string) string {
|
||||
}
|
||||
|
||||
func (ws *Server) authenticateListener(wsSession *websocketSession, token string) {
|
||||
ws.logger.Debug("authenticateListener",
|
||||
mlog.String("token", token),
|
||||
mlog.String("wsSession.userID", wsSession.userID),
|
||||
)
|
||||
if wsSession.isAuthenticated() {
|
||||
// Do not allow multiple auth calls (for security)
|
||||
ws.logger.Debug(
|
||||
@ -476,38 +459,6 @@ func (ws *Server) authenticateListener(wsSession *websocketSession, token string
|
||||
ws.logger.Debug("authenticateListener: Authenticated", mlog.String("userID", userID), mlog.Stringer("client", wsSession.client.RemoteAddr()))
|
||||
}
|
||||
|
||||
func (ws *Server) SetHub(hub Hub) {
|
||||
ws.hub = hub
|
||||
ws.hub.SetReceiveWSMessage(func(data []byte) {
|
||||
var msg clusterUpdateMsg
|
||||
err := json.Unmarshal(data, &msg)
|
||||
if err != nil {
|
||||
log.Printf("unable to unmarshal cluster message")
|
||||
return
|
||||
}
|
||||
|
||||
listeners := ws.getListenersForBlock(msg.BlockID)
|
||||
log.Printf("%d listener(s) for blockID: %s", len(listeners), msg.BlockID)
|
||||
listeners = append(listeners, ws.getListenersForWorkspace(msg.WorkspaceID)...)
|
||||
log.Printf("%d listener(s) for workspaceID: %s", len(listeners), msg.WorkspaceID)
|
||||
|
||||
message := UpdateMsg{
|
||||
Action: msg.Action,
|
||||
Block: msg.Block,
|
||||
}
|
||||
|
||||
for _, listener := range listeners {
|
||||
log.Printf("Broadcast change, workspaceID: %s, blockID: %s, remoteAddr: %s", msg.WorkspaceID, msg.BlockID, listener.RemoteAddr())
|
||||
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
log.Printf("broadcast error: %v", err)
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getListenersForBlock returns the listeners subscribed to a
|
||||
// block changes.
|
||||
func (ws *Server) getListenersForBlock(blockID string) []*wsClient {
|
||||
@ -537,7 +488,7 @@ func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) {
|
||||
blockIDsToNotify := []string{block.ID, block.ParentID}
|
||||
|
||||
message := UpdateMsg{
|
||||
Action: "UPDATE_BLOCK",
|
||||
Action: websocketActionUpdateBlock,
|
||||
Block: block,
|
||||
}
|
||||
|
||||
@ -553,14 +504,6 @@ func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) {
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
mlog.String("blockID", blockID),
|
||||
)
|
||||
|
||||
if ws.hub != nil {
|
||||
data, err := json.Marshal(clusterUpdateMsg{UpdateMsg: message, WorkspaceID: workspaceID, BlockID: blockID})
|
||||
if err != nil {
|
||||
log.Printf("unable to serialize websocket message %v with the error: %v", message, err)
|
||||
}
|
||||
ws.hub.SendWSMessage(data)
|
||||
}
|
||||
}
|
||||
|
||||
for _, listener := range listeners {
|
@ -14,7 +14,7 @@ import (
|
||||
|
||||
func TestWorkspaceSubscription(t *testing.T) {
|
||||
server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{})
|
||||
client := &wsClient{&websocket.Conn{}, &sync.Mutex{}, []string{}, []string{}}
|
||||
client := &wsClient{&websocket.Conn{}, sync.Mutex{}, []string{}, []string{}}
|
||||
session := &websocketSession{client: client}
|
||||
workspaceID := "fake-workspace-id"
|
||||
|
||||
@ -100,7 +100,7 @@ func TestWorkspaceSubscription(t *testing.T) {
|
||||
|
||||
func TestBlocksSubscription(t *testing.T) {
|
||||
server := NewServer(&auth.Auth{}, "token", false, &mlog.Logger{})
|
||||
client := &wsClient{&websocket.Conn{}, &sync.Mutex{}, []string{}, []string{}}
|
||||
client := &wsClient{&websocket.Conn{}, sync.Mutex{}, []string{}, []string{}}
|
||||
session := &websocketSession{client: client}
|
||||
blockID1 := "block1"
|
||||
blockID2 := "block2"
|
@ -18,6 +18,7 @@ interface BlockPatch {
|
||||
deletedFields?: string[]
|
||||
deleteAt?: number
|
||||
}
|
||||
|
||||
interface Block {
|
||||
id: string
|
||||
parentId: string
|
||||
|
@ -96,4 +96,3 @@ export const getCurrentBoard = createSelector(
|
||||
return boards[boardId] || templates[boardId]
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -3,6 +3,12 @@
|
||||
import marked from 'marked'
|
||||
import {IntlShape} from 'react-intl'
|
||||
|
||||
import {Block} from './blocks/block'
|
||||
import {createBoard} from './blocks/board'
|
||||
import {createBoardView} from './blocks/boardView'
|
||||
import {createCard} from './blocks/card'
|
||||
import {createCommentBlock} from './blocks/commentBlock'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
msCrypto: Crypto
|
||||
@ -416,6 +422,21 @@ class Utils {
|
||||
static isFocalboardPlugin(): boolean {
|
||||
return Boolean((window as any).isFocalboardPlugin)
|
||||
}
|
||||
|
||||
static fixBlock(block: Block): Block {
|
||||
switch (block.type) {
|
||||
case 'board':
|
||||
return createBoard(block)
|
||||
case 'view':
|
||||
return createBoardView(block)
|
||||
case 'card':
|
||||
return createCard(block)
|
||||
case 'comment':
|
||||
return createCommentBlock(block)
|
||||
default:
|
||||
return block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {Utils}
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Utils} from './utils'
|
||||
import {Block} from './blocks/block'
|
||||
import {OctoUtils} from './octoUtils'
|
||||
@ -19,6 +20,18 @@ type WSMessage = {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const ACTION_UPDATE_BLOCK = 'UPDATE_BLOCK'
|
||||
export const ACTION_AUTH = 'AUTH'
|
||||
export const ACTION_SUBSCRIBE_BLOCKS = 'SUBSCRIBE_BLOCKS'
|
||||
export const ACTION_SUBSCRIBE_WORKSPACE = 'SUBSCRIBE_WORKSPACE'
|
||||
export const ACTION_UNSUBSCRIBE_WORKSPACE = 'UNSUBSCRIBE_WORKSPACE'
|
||||
export const ACTION_UNSUBSCRIBE_BLOCKS = 'UNSUBSCRIBE_BLOCKS'
|
||||
|
||||
// The Mattermost websocket client interface
|
||||
export interface MMWebSocketClient {
|
||||
sendMessage(action: string, data: any, responseCallback?: () => void): void
|
||||
}
|
||||
|
||||
type OnChangeHandler = (client: WSClient, blocks: Block[]) => void
|
||||
type OnReconnectHandler = (client: WSClient) => void
|
||||
type OnStateChangeHandler = (client: WSClient, state: 'init' | 'open' | 'close') => void
|
||||
@ -26,6 +39,8 @@ type OnErrorHandler = (client: WSClient, e: Event) => void
|
||||
|
||||
class WSClient {
|
||||
ws: WebSocket|null = null
|
||||
client: MMWebSocketClient|null = null
|
||||
clientPrefix = ''
|
||||
serverUrl: string
|
||||
state: 'init'|'open'|'close' = 'init'
|
||||
onStateChange: OnStateChangeHandler[] = []
|
||||
@ -42,6 +57,21 @@ class WSClient {
|
||||
Utils.log(`WSClient serverUrl: ${this.serverUrl}`)
|
||||
}
|
||||
|
||||
initPlugin(pluginId: string, client: MMWebSocketClient): void {
|
||||
this.clientPrefix = `custom_${pluginId}_`
|
||||
this.client = client
|
||||
Utils.log(`WSClient initialised for plugin id "${pluginId}"`)
|
||||
}
|
||||
|
||||
sendCommand(command: WSCommand): void {
|
||||
if (this.client !== null) {
|
||||
const {action, ...data} = command
|
||||
this.client.sendMessage(this.clientPrefix + action, data)
|
||||
}
|
||||
|
||||
this.ws?.send(JSON.stringify(command))
|
||||
}
|
||||
|
||||
addOnChange(handler: OnChangeHandler): void {
|
||||
this.onChange.push(handler)
|
||||
}
|
||||
@ -87,6 +117,13 @@ class WSClient {
|
||||
}
|
||||
|
||||
open(): void {
|
||||
// if running in plugin mode, no ws configuration needs to be done
|
||||
if (this.client !== null) {
|
||||
this.state = 'open'
|
||||
Utils.log('Application in plugin mode, reusing Mattermost WS connection')
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(this.serverUrl)
|
||||
const protocol = (url.protocol === 'https:') ? 'wss:' : 'ws:'
|
||||
const wsServerUrl = `${protocol}//${url.host}${url.pathname.replace(/\/$/, '')}/ws`
|
||||
@ -142,8 +179,8 @@ class WSClient {
|
||||
}
|
||||
|
||||
switch (message.action) {
|
||||
case 'UPDATE_BLOCK':
|
||||
this.queueUpdateNotification(message.block!)
|
||||
case ACTION_UPDATE_BLOCK:
|
||||
this.updateBlockHandler(message)
|
||||
break
|
||||
default:
|
||||
Utils.logError(`Unexpected action: ${message.action}`)
|
||||
@ -154,8 +191,16 @@ class WSClient {
|
||||
}
|
||||
}
|
||||
|
||||
hasConn(): boolean {
|
||||
return this.ws !== null || this.client !== null
|
||||
}
|
||||
|
||||
updateBlockHandler(message: WSMessage): void {
|
||||
this.queueUpdateNotification(Utils.fixBlock(message.block!))
|
||||
}
|
||||
|
||||
authenticate(workspaceId: string, token: string): void {
|
||||
if (!this.ws) {
|
||||
if (!this.hasConn()) {
|
||||
Utils.assertFailure('WSClient.addBlocks: ws is not open')
|
||||
return
|
||||
}
|
||||
@ -164,71 +209,72 @@ class WSClient {
|
||||
return
|
||||
}
|
||||
const command = {
|
||||
action: 'AUTH',
|
||||
action: ACTION_AUTH,
|
||||
token,
|
||||
workspaceId,
|
||||
}
|
||||
this.ws.send(JSON.stringify(command))
|
||||
|
||||
this.sendCommand(command)
|
||||
}
|
||||
|
||||
subscribeToBlocks(workspaceId: string, blockIds: string[], readToken = ''): void {
|
||||
if (!this.ws) {
|
||||
if (!this.hasConn()) {
|
||||
Utils.assertFailure('WSClient.subscribeToBlocks: ws is not open')
|
||||
return
|
||||
}
|
||||
|
||||
const command: WSCommand = {
|
||||
action: 'SUBSCRIBE_BLOCKS',
|
||||
action: ACTION_SUBSCRIBE_BLOCKS,
|
||||
blockIds,
|
||||
workspaceId,
|
||||
readToken,
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(command))
|
||||
this.sendCommand(command)
|
||||
}
|
||||
|
||||
unsubscribeToWorkspace(workspaceId: string): void {
|
||||
if (!this.ws) {
|
||||
if (!this.hasConn()) {
|
||||
Utils.assertFailure('WSClient.subscribeToWorkspace: ws is not open')
|
||||
return
|
||||
}
|
||||
|
||||
const command: WSCommand = {
|
||||
action: 'UNSUBSCRIBE_WORKSPACE',
|
||||
action: ACTION_UNSUBSCRIBE_WORKSPACE,
|
||||
workspaceId,
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(command))
|
||||
this.sendCommand(command)
|
||||
}
|
||||
|
||||
subscribeToWorkspace(workspaceId: string): void {
|
||||
if (!this.ws) {
|
||||
if (!this.hasConn()) {
|
||||
Utils.assertFailure('WSClient.subscribeToWorkspace: ws is not open')
|
||||
return
|
||||
}
|
||||
|
||||
const command: WSCommand = {
|
||||
action: 'SUBSCRIBE_WORKSPACE',
|
||||
action: ACTION_SUBSCRIBE_WORKSPACE,
|
||||
workspaceId,
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(command))
|
||||
this.sendCommand(command)
|
||||
}
|
||||
|
||||
unsubscribeFromBlocks(workspaceId: string, blockIds: string[], readToken = ''): void {
|
||||
if (!this.ws) {
|
||||
if (!this.hasConn()) {
|
||||
Utils.assertFailure('WSClient.removeBlocks: ws is not open')
|
||||
return
|
||||
}
|
||||
|
||||
const command: WSCommand = {
|
||||
action: 'UNSUBSCRIBE_BLOCKS',
|
||||
action: ACTION_UNSUBSCRIBE_BLOCKS,
|
||||
blockIds,
|
||||
workspaceId,
|
||||
readToken,
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(command))
|
||||
this.sendCommand(command)
|
||||
}
|
||||
|
||||
private queueUpdateNotification(block: Block) {
|
||||
@ -255,11 +301,11 @@ class WSClient {
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (!this.ws) {
|
||||
if (!this.hasConn()) {
|
||||
return
|
||||
}
|
||||
|
||||
Utils.log(`WSClient close: ${this.ws.url}`)
|
||||
Utils.log(`WSClient close: ${this.ws?.url}`)
|
||||
|
||||
// Use this sequence so the onclose method doesn't try to re-open
|
||||
const ws = this.ws
|
||||
@ -268,6 +314,12 @@ class WSClient {
|
||||
this.onReconnect = []
|
||||
this.onStateChange = []
|
||||
this.onError = []
|
||||
|
||||
// if running in plugin mode, nothing else needs to be done
|
||||
if (this.client) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
ws?.close()
|
||||
} catch {
|
||||
|
Loading…
Reference in New Issue
Block a user