Skip to content

Commit

Permalink
Add Embedding support (#69)
Browse files Browse the repository at this point in the history
* Add Embedding support

* Excluding tests files from the typescript type checking
  • Loading branch information
jespino committed May 29, 2020
1 parent 346a6e4 commit e0a8486
Show file tree
Hide file tree
Showing 23 changed files with 1,625 additions and 75 deletions.
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -17,7 +17,7 @@ require (
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/pelletier/go-toml v1.8.0 // indirect
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.6.0 // indirect
github.com/stretchr/testify v1.6.0
go.uber.org/zap v1.15.0 // indirect
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
Expand Down
6 changes: 6 additions & 0 deletions plugin.json
Expand Up @@ -27,6 +27,12 @@
"help_text": "The URL for an on-premise Jitsi server. For example, https://jitsi.example.com.",
"placeholder": "https://jitsi.example.com"
},
{
"key": "JitsiEmbedded",
"display_name": "Embed Jitsi video inside Mattermost",
"type": "bool",
"help_text": "(Experimental) When true, Jitsi video is embedded as a floating window inside Mattermost."
},
{
"key": "JitsiJWT",
"display_name": "Use JWT Authentication for Jitsi",
Expand Down
37 changes: 36 additions & 1 deletion server/api.go
Expand Up @@ -6,11 +6,18 @@ import (
"io/ioutil"
"log"
"net/http"
"sync"

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

const externalAPICacheTTL = 3600000

var externalAPICache []byte
var externalAPILastUpdate int64
var externalAPICacheMutex sync.Mutex

type StartMeetingRequest struct {
ChannelID string `json:"channel_id"`
Personal bool `json:"personal"`
Expand All @@ -35,6 +42,8 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
p.handleStartMeeting(w, r)
case "/api/v1/config":
p.handleConfig(w, r)
case "/jitsi_meet_external_api.js":
p.proxyExternalAPIjs(w, r)
default:
http.NotFound(w, r)
}
Expand Down Expand Up @@ -64,6 +73,32 @@ func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(b)
}

func (p *Plugin) proxyExternalAPIjs(w http.ResponseWriter, r *http.Request) {
externalAPICacheMutex.Lock()
defer externalAPICacheMutex.Unlock()

if externalAPICache != nil && externalAPILastUpdate > (model.GetMillis()-externalAPICacheTTL) {
w.Header().Set("Content-Type", "application/javascript")
_, _ = w.Write(externalAPICache)
return
}
resp, err := http.Get(p.getConfiguration().JitsiURL + "/external_api.js")
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
externalAPICache = body
externalAPILastUpdate = model.GetMillis()
w.Header().Set("Content-Type", "application/javascript")
_, _ = w.Write(body)
}

func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
if err := p.getConfiguration().IsValid(); err != nil {
http.Error(w, err.Error(), http.StatusTeapot)
Expand Down Expand Up @@ -197,7 +232,7 @@ func (p *Plugin) handleEnrichMeetingJwt(w http.ResponseWriter, r *http.Request)
return
}

b, err2 := json.Marshal(map[string]string{"jwt": meetingJWT})
b, err2 := json.Marshal(map[string]interface{}{"jwt": meetingJWT})
if err2 != nil {
log.Printf("Error marshaling the JWT json: %v", err2)
http.Error(w, "Internal error", http.StatusInternalServerError)
Expand Down
16 changes: 14 additions & 2 deletions server/command.go
Expand Up @@ -16,6 +16,7 @@ const commandHelp = `* |/jitsi| - Create a new meeting
* |/jitsi settings [setting] [value]| - Update your user settings (see below for options)
###### Jitsi Settings:
* |/jitsi settings embedded [true/false]|: When true, Jitsi meeting is embedded as a floating window inside Mattermost. When false, Jitsi meeting opens in a new window.
* |/jitsi settings naming_scheme [words/uuid/mattermost/ask]|: Select how meeting names are generated with one of these options:
* |words|: Random English words in title case (e.g. PlayfulDragonsObserveCuriously)
* |uuid|: UUID (universally unique identifier)
Expand Down Expand Up @@ -132,7 +133,7 @@ func (p *Plugin) executeSettingsCommand(c *plugin.Context, args *model.CommandAr
}

if len(parameters) == 0 {
text = fmt.Sprintf("###### Jitsi Settings:\n* Naming Scheme: `%s`", userConfig.NamingScheme)
text = fmt.Sprintf("###### Jitsi Settings:\n* Embedded: `%v`\n* Naming Scheme: `%s`", userConfig.Embedded, userConfig.NamingScheme)
post := &model.Post{
UserId: args.UserId,
ChannelId: args.ChannelId,
Expand All @@ -148,6 +149,17 @@ func (p *Plugin) executeSettingsCommand(c *plugin.Context, args *model.CommandAr
}

switch parameters[0] {
case "embedded":
switch parameters[1] {
case "true":
userConfig.Embedded = true
case "false":
userConfig.Embedded = false
default:
text = "Invalid `embedded` value, use `true` or `false`."
userConfig = nil
break
}
case "naming_scheme":
switch parameters[1] {
case jitsiNameSchemaAsk:
Expand All @@ -163,7 +175,7 @@ func (p *Plugin) executeSettingsCommand(c *plugin.Context, args *model.CommandAr
userConfig = nil
}
default:
text = "Invalid config field, use `naming_scheme`."
text = "Invalid config field, use `embedded` or `naming_scheme`."
userConfig = nil
}

Expand Down
195 changes: 195 additions & 0 deletions server/command_test.go
@@ -0,0 +1,195 @@
package main

import (
"encoding/json"
"strings"
"testing"

"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
"github.com/mattermost/mattermost-server/v5/plugin/plugintest"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestCommandHelp(t *testing.T) {
p := Plugin{
configuration: &configuration{
JitsiURL: "http://test",
},
}
apiMock := plugintest.API{}
defer apiMock.AssertExpectations(t)

p.SetAPI(&apiMock)

helpText := strings.Replace(`###### Mattermost Jitsi Plugin - Slash Command Help
* |/jitsi| - Create a new meeting
* |/jitsi [topic]| - Create a new meeting with specified topic
* |/jitsi help| - Show this help text
* |/jitsi settings| - View your current user settings for the Jitsi plugin
* |/jitsi settings [setting] [value]| - Update your user settings (see below for options)
###### Jitsi Settings:
* |/jitsi settings embedded [true/false]|: When true, Jitsi meeting is embedded as a floating window inside Mattermost. When false, Jitsi meeting opens in a new window.
* |/jitsi settings naming_scheme [words/uuid/mattermost/ask]|: Select how meeting names are generated with one of these options:
* |words|: Random English words in title case (e.g. PlayfulDragonsObserveCuriously)
* |uuid|: UUID (universally unique identifier)
* |mattermost|: Mattermost specific names. Combination of team name, channel name and random text in public and private channels; personal meeting name in direct and group messages channels.
* |ask|: The plugin asks you to select the name every time you start a meeting`, "|", "`", -1)

apiMock.On("SendEphemeralPost", "test-user", &model.Post{
UserId: "test-user",
ChannelId: "test-channel",
Message: helpText,
}).Return(nil)
response, err := p.ExecuteCommand(&plugin.Context{}, &model.CommandArgs{UserId: "test-user", ChannelId: "test-channel", Command: "/jitsi help"})
require.Equal(t, response, &model.CommandResponse{})
require.Nil(t, err)
}

func TestCommandSetings(t *testing.T) {
p := Plugin{
configuration: &configuration{
JitsiURL: "http://test",
JitsiEmbedded: false,
JitsiNamingScheme: "mattermost",
},
}

response, err := p.ExecuteCommand(&plugin.Context{}, &model.CommandArgs{UserId: "test-user", ChannelId: "test-channel", Command: "jitsi help"})
require.Equal(t, response, &model.CommandResponse{})
require.Nil(t, err)

tests := []struct {
name string
command string
output string
newConfig *UserConfig
}{
{
name: "set valid setting with valid value",
command: "/jitsi settings embedded true",
output: "Jitsi settings updated",
newConfig: &UserConfig{Embedded: true, NamingScheme: "mattermost"},
},
{
name: "set valid setting with invalid value (embedded)",
command: "/jitsi settings embedded yes",
output: "Invalid `embedded` value, use `true` or `false`.",
newConfig: nil,
},
{
name: "set valid setting with invalid value (naming_scheme)",
command: "/jitsi settings naming_scheme yes",
output: "Invalid `naming_scheme` value, use `ask`, `english-titlecase`, `uuid` or `mattermost`.",
newConfig: nil,
},
{
name: "set invalid setting",
command: "/jitsi settings other true",
output: "Invalid config field, use `embedded` or `naming_scheme`.",
newConfig: nil,
},
{
name: "set invalid number of parameters",
command: "/jitsi settings other",
output: "Invalid settings parameters\n",
newConfig: nil,
},
{
name: "get current user settings",
command: "/jitsi settings",
output: "###### Jitsi Settings:\n* Embedded: `false`\n* Naming Scheme: `mattermost`",
newConfig: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
apiMock := plugintest.API{}
defer apiMock.AssertExpectations(t)
p.SetAPI(&apiMock)
apiMock.On("KVGet", "config_test-user", mock.Anything).Return(nil, nil)
apiMock.On("SendEphemeralPost", "test-user", &model.Post{
UserId: "test-user",
ChannelId: "test-channel",
Message: tt.output,
}).Return(nil)
if tt.newConfig != nil {
b, _ := json.Marshal(tt.newConfig)
apiMock.On("KVSet", "config_test-user", b).Return(nil)
var nilMap map[string]interface{}
apiMock.On("PublishWebSocketEvent", configChangeEvent, nilMap, &model.WebsocketBroadcast{UserId: "test-user"})
}
response, err := p.ExecuteCommand(&plugin.Context{}, &model.CommandArgs{UserId: "test-user", ChannelId: "test-channel", Command: tt.command})
require.Equal(t, response, &model.CommandResponse{})
require.Nil(t, err)
})
}
}

func TestCommandStartMeeting(t *testing.T) {
p := Plugin{
configuration: &configuration{
JitsiURL: "http://test",
},
}

t.Run("meeting without topic and ask configuration", func(t *testing.T) {
apiMock := plugintest.API{}
defer apiMock.AssertExpectations(t)
p.SetAPI(&apiMock)

apiMock.On("SendEphemeralPost", "test-user", mock.MatchedBy(func(post *model.Post) bool {
return post.Props["attachments"].([]*model.SlackAttachment)[0].Text == "Select type of meeting you want to start"
})).Return(nil)
apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user"}, nil)
apiMock.On("GetChannel", "test-channel").Return(&model.Channel{Id: "test-channel"}, nil)
apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user"}, nil)
b, _ := json.Marshal(UserConfig{Embedded: false, NamingScheme: "ask"})
apiMock.On("KVGet", "config_test-user", mock.Anything).Return(b, nil)
config := model.Config{}
config.SetDefaults()
apiMock.On("GetConfig").Return(&config, nil)

response, err := p.ExecuteCommand(&plugin.Context{}, &model.CommandArgs{UserId: "test-user", ChannelId: "test-channel", Command: "/jitsi"})
require.Equal(t, response, &model.CommandResponse{})
require.Nil(t, err)
})

t.Run("meeting without topic and no ask configuration", func(t *testing.T) {
apiMock := plugintest.API{}
defer apiMock.AssertExpectations(t)
p.SetAPI(&apiMock)

apiMock.On("CreatePost", mock.MatchedBy(func(post *model.Post) bool {
return strings.HasPrefix(post.Props["meeting_link"].(string), "http://test/")
})).Return(&model.Post{}, nil)
apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user"}, nil)
apiMock.On("GetChannel", "test-channel").Return(&model.Channel{Id: "test-channel"}, nil)
apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user"}, nil)
apiMock.On("KVGet", "config_test-user", mock.Anything).Return(nil, nil)

response, err := p.ExecuteCommand(&plugin.Context{}, &model.CommandArgs{UserId: "test-user", ChannelId: "test-channel", Command: "/jitsi"})
require.Equal(t, response, &model.CommandResponse{})
require.Nil(t, err)
})

t.Run("meeting with topic", func(t *testing.T) {
apiMock := plugintest.API{}
defer apiMock.AssertExpectations(t)
p.SetAPI(&apiMock)

apiMock.On("CreatePost", mock.MatchedBy(func(post *model.Post) bool { return post.Props["meeting_link"] == "http://test/topic" })).Return(&model.Post{}, nil)
apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user"}, nil)
apiMock.On("GetChannel", "test-channel").Return(&model.Channel{Id: "test-channel"}, nil)
apiMock.On("GetUser", "test-user").Return(&model.User{Id: "test-user"}, nil)
apiMock.On("KVGet", "config_test-user", mock.Anything).Return(nil, nil)

response, err := p.ExecuteCommand(&plugin.Context{}, &model.CommandArgs{UserId: "test-user", ChannelId: "test-channel", Command: "/jitsi topic"})
require.Equal(t, response, &model.CommandResponse{})
require.Nil(t, err)
})

}
1 change: 1 addition & 0 deletions server/configuration.go
Expand Up @@ -26,6 +26,7 @@ import (
type configuration struct {
JitsiURL string
JitsiJWT bool
JitsiEmbedded bool
JitsiAppID string
JitsiAppSecret string
JitsiLinkValidTime int
Expand Down
8 changes: 8 additions & 0 deletions server/manifest.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions server/plugin.go
Expand Up @@ -23,6 +23,7 @@ const jitsiNameSchemaMattermost = "mattermost"
const configChangeEvent = "config_update"

type UserConfig struct {
Embedded bool `json:"embedded"`
NamingScheme string `json:"naming_scheme"`
}

Expand Down Expand Up @@ -207,6 +208,7 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI

meetingURL = meetingURL + "?jwt=" + jwtToken
}
meetingURL = meetingURL + "#config.callDisplayName=\"" + meetingTopic + "\""

meetingUntil := ""
if JWTMeeting {
Expand Down Expand Up @@ -347,6 +349,7 @@ func (p *Plugin) getUserConfig(userID string) (*UserConfig, error) {

if data == nil {
return &UserConfig{
Embedded: p.getConfiguration().JitsiEmbedded,
NamingScheme: p.getConfiguration().JitsiNamingScheme,
}, nil
}
Expand Down

0 comments on commit e0a8486

Please sign in to comment.