1
0
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:
Miguel de la Cruz 2021-08-27 10:59:14 +02:00 committed by GitHub
parent c3f5993126
commit e10229031f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 504 additions and 155 deletions

View File

@ -1,6 +1,6 @@
module github.com/mattermost/focalboard/linux
go 1.15
go 1.16
replace github.com/mattermost/focalboard/server => ../server

View File

@ -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=

View File

@ -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

View File

@ -1,6 +1,6 @@
module github.com/mattermost/focalboard/mattermost-plugin
go 1.12
go 1.16
replace github.com/mattermost/focalboard/server => ../server

View File

@ -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=

View File

@ -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 {

View File

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

View File

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

View File

@ -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,

View File

@ -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

View File

@ -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=

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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"

View File

@ -18,6 +18,7 @@ interface BlockPatch {
deletedFields?: string[]
deleteAt?: number
}
interface Block {
id: string
parentId: string

View File

@ -96,4 +96,3 @@ export const getCurrentBoard = createSelector(
return boards[boardId] || templates[boardId]
},
)

View File

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

View File

@ -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 {