Skip to content

Commit

Permalink
Add slash command (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
jespino committed May 29, 2020
1 parent b848769 commit a8e1cdf
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 11 deletions.
36 changes: 34 additions & 2 deletions server/api.go
Expand Up @@ -33,11 +33,37 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
p.handleEnrichMeetingJwt(w, r)
case "/api/v1/meetings":
p.handleStartMeeting(w, r)
case "/api/v1/config":
p.handleConfig(w, r)
default:
http.NotFound(w, r)
}
}

func (p *Plugin) handleConfig(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-Id")

if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

config, err := p.getUserConfig(userID)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}

b, err2 := json.Marshal(config)
if err2 != nil {
log.Printf("Error marshaling the Config to json: %v", err2)
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
}

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 @@ -89,7 +115,13 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
return
}

if p.getConfiguration().JitsiNamingScheme == jitsiNameSchemaAsk && action.PostId == "" {
userConfig, err := p.getUserConfig(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if userConfig.NamingScheme == jitsiNameSchemaAsk && action.PostId == "" {
err := p.askMeetingType(user, channel)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand All @@ -100,7 +132,7 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
} else {
var meetingID string
var err error
if p.getConfiguration().JitsiNamingScheme == jitsiNameSchemaAsk && action.PostId != "" {
if userConfig.NamingScheme == jitsiNameSchemaAsk && action.PostId != "" {
meetingID, err = p.startMeeting(user, channel, action.Context.MeetingID, action.Context.MeetingTopic, action.Context.Personal)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand Down
143 changes: 142 additions & 1 deletion server/command.go
Expand Up @@ -9,6 +9,18 @@ import (
)

const jitsiCommand = "jitsi"
const commandHelp = `* |/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 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`

func commandError(channelID string, detailedError string) (*model.CommandResponse, *model.AppError) {
return &model.CommandResponse{
Expand All @@ -21,7 +33,43 @@ func commandError(channelID string, detailedError string) (*model.CommandRespons
}
}

func createJitsiCommand() *model.Command {
return &model.Command{
Trigger: jitsiCommand,
AutoComplete: true,
AutoCompleteDesc: "Start a Jitsi meeting in current channel. Other available commands: help, settings",
AutoCompleteHint: "[command]",
}
}

func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
split := strings.Fields(args.Command)
command := split[0]
var parameters []string
action := ""
if len(split) > 1 {
action = split[1]
}
if len(split) > 2 {
parameters = split[2:]
}

if command != "/"+jitsiCommand {
return &model.CommandResponse{}, nil
}

if action == "help" {
return p.executeHelpCommand(c, args)
}

if action == "settings" {
return p.executeSettingsCommand(c, args, parameters)
}

return p.executeStartMeetingCommand(c, args)
}

func (p *Plugin) executeStartMeetingCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
input := strings.TrimSpace(strings.TrimPrefix(args.Command, "/"+jitsiCommand))

user, appErr := p.API.GetUser(args.UserId)
Expand All @@ -34,7 +82,12 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
return commandError(args.ChannelId, fmt.Sprintf("getChannel() threw error: %s", appErr))
}

if p.getConfiguration().JitsiNamingScheme == jitsiNameSchemaAsk && input == "" {
userConfig, err := p.getUserConfig(args.UserId)
if err != nil {
return commandError(args.ChannelId, fmt.Sprintf("getChannel() threw error: %s", err))
}

if userConfig.NamingScheme == jitsiNameSchemaAsk && input == "" {
if err := p.askMeetingType(user, channel); err != nil {
return commandError(args.ChannelId, fmt.Sprintf("startMeeting() threw error: %s", appErr))
}
Expand All @@ -46,3 +99,91 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo

return &model.CommandResponse{}, nil
}

func (p *Plugin) executeHelpCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
text := "###### Mattermost Jitsi Plugin - Slash Command Help\n" + strings.Replace(commandHelp, "|", "`", -1)
post := &model.Post{
UserId: args.UserId,
ChannelId: args.ChannelId,
Message: text,
}
_ = p.API.SendEphemeralPost(args.UserId, post)

return &model.CommandResponse{}, nil
}

func (p *Plugin) settingsError(userID string, channelID string, errorText string) (*model.CommandResponse, *model.AppError) {
post := &model.Post{
UserId: userID,
ChannelId: channelID,
Message: errorText,
}
_ = p.API.SendEphemeralPost(userID, post)

return &model.CommandResponse{}, nil
}

func (p *Plugin) executeSettingsCommand(c *plugin.Context, args *model.CommandArgs, parameters []string) (*model.CommandResponse, *model.AppError) {
text := ""

userConfig, err := p.getUserConfig(args.UserId)
if err != nil {
return p.settingsError(args.UserId, args.ChannelId, err.Error())
}

if len(parameters) == 0 {
text = fmt.Sprintf("###### Jitsi Settings:\n* Naming Scheme: `%s`", userConfig.NamingScheme)
post := &model.Post{
UserId: args.UserId,
ChannelId: args.ChannelId,
Message: text,
}
_ = p.API.SendEphemeralPost(args.UserId, post)

return &model.CommandResponse{}, nil
}

if len(parameters) != 2 {
return p.settingsError(args.UserId, args.ChannelId, "Invalid settings parameters\n")
}

switch parameters[0] {
case "naming_scheme":
switch parameters[1] {
case "ask":
userConfig.NamingScheme = "ask"
case "english-titlecase":
userConfig.NamingScheme = "english-titlecase"
case "uuid":
userConfig.NamingScheme = "uuid"
case "mattermost":
userConfig.NamingScheme = "mattermost"
default:
text = "Invalid `naming_scheme` value, use `ask`, `english-titlecase`, `uuid` or `mattermost`."
userConfig = nil
break
}
default:
text = "Invalid config field, use `naming_scheme`."
userConfig = nil
break
}

if userConfig == nil {
return p.settingsError(args.UserId, args.ChannelId, text)
}

err = p.setUserConfig(args.UserId, userConfig)
if err != nil {
return p.settingsError(args.UserId, args.ChannelId, err.Error())
}

post := &model.Post{
UserId: args.UserId,
ChannelId: args.ChannelId,
Message: "Jitsi settings updated",
}
_ = p.API.SendEphemeralPost(args.UserId, post)

return &model.CommandResponse{}, nil
}
2 changes: 2 additions & 0 deletions server/configuration.go
Expand Up @@ -8,6 +8,7 @@ import (
"net/url"
"reflect"

"github.com/mattermost/mattermost-server/v5/model"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -102,6 +103,7 @@ func (p *Plugin) setConfiguration(configuration *configuration) {
panic("setConfiguration called with the existing configuration")
}

p.API.PublishWebSocketEvent(configChangeEvent, nil, &model.WebsocketBroadcast{})
p.configuration = configuration
}

Expand Down
54 changes: 47 additions & 7 deletions server/plugin.go
Expand Up @@ -20,6 +20,11 @@ const jitsiNameSchemaAsk = "ask"
const jitsiNameSchemaEnglish = "english-titlecase"
const jitsiNameSchemaUUID = "uuid"
const jitsiNameSchemaMattermost = "mattermost"
const configChangeEvent = "config_update"

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

type Plugin struct {
plugin.MattermostPlugin
Expand All @@ -38,11 +43,7 @@ func (p *Plugin) OnActivate() error {
return err
}

if err := p.API.RegisterCommand(&model.Command{
Trigger: jitsiCommand,
AutoComplete: true,
AutoCompleteDesc: "Start a Jitsi meeting for the current channel. Optionally, append desired meeting topic after the command",
}); err != nil {
if err := p.API.RegisterCommand(createJitsiCommand()); err != nil {
return err
}

Expand Down Expand Up @@ -149,9 +150,12 @@ func (p *Plugin) startMeeting(user *model.User, channel *model.Channel, meetingI
meetingPersonal := false

if len(meetingTopic) < 1 {
namingScheme := p.getConfiguration().JitsiNamingScheme
userConfig, err := p.getUserConfig(user.Id)
if err != nil {
return "", err
}

switch namingScheme {
switch userConfig.NamingScheme {
case jitsiNameSchemaEnglish:
meetingID = generateEnglishTitleName()
case jitsiNameSchemaUUID:
Expand Down Expand Up @@ -334,3 +338,39 @@ func (p *Plugin) askMeetingType(user *model.User, channel *model.Channel) error

return nil
}

func (p *Plugin) getUserConfig(userID string) (*UserConfig, error) {
data, appErr := p.API.KVGet("config_" + userID)
if appErr != nil {
return nil, appErr
}

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

var userConfig UserConfig
err := json.Unmarshal(data, &userConfig)
if err != nil {
return nil, err
}

return &userConfig, nil
}

func (p *Plugin) setUserConfig(userID string, config *UserConfig) error {
b, err := json.Marshal(config)
if err != nil {
return err
}

appErr := p.API.KVSet("config_"+userID, b)
if appErr != nil {
return appErr
}

p.API.PublishWebSocketEvent(configChangeEvent, nil, &model.WebsocketBroadcast{UserId: userID})
return nil
}
9 changes: 9 additions & 0 deletions webapp/src/action_types/index.ts
@@ -0,0 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {id as pluginId} from '../manifest';

export default {
OPEN_MEETING: pluginId + '_open_meeting',
CONFIG_RECEIVED: pluginId + '_config_received'
};
17 changes: 17 additions & 0 deletions webapp/src/actions/index.ts
Expand Up @@ -2,6 +2,8 @@ import {PostTypes} from 'mattermost-redux/action_types';
import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
import {Post} from 'mattermost-redux/types/posts';

import ActionTypes from '../action_types';

import Client from '../client';

export function startMeeting(channelId: string, personal: boolean = false, topic: string = '', meetingId: string = '') {
Expand Down Expand Up @@ -64,3 +66,18 @@ export function enrichMeetingJwt(meetingJwt: string) {
}
};
}

export function loadConfig() {
return async (dispatch: DispatchFunc) => {
try {
const data = await Client.loadConfig();
dispatch({
type: ActionTypes.CONFIG_RECEIVED,
data
});
return {data};
} catch (error) {
return {error};
}
};
}
4 changes: 4 additions & 0 deletions webapp/src/client/client.ts
Expand Up @@ -21,6 +21,10 @@ export default class Client {
return this.doPost(`${this.url}/api/v1/meetings/enrich`, {jwt: meetingJwt});
}

loadConfig = async () => {
return this.doPost(`${this.url}/api/v1/config`, {});
}

doPost = async (url: string, body: any, headers: any = {}) => {
const options = {
method: 'post',
Expand Down
6 changes: 5 additions & 1 deletion webapp/src/index.tsx
Expand Up @@ -7,10 +7,12 @@ import {Channel} from 'mattermost-redux/types/channels';

import Icon from './components/icon';
import PostTypeJitsi from './components/post_type_jitsi';
import {startMeeting} from './actions';
import reducer from './reducers';
import {startMeeting, loadConfig} from './actions';

class PluginClass {
initialize(registry: any, store: any) {
registry.registerReducer(reducer);
registry.registerChannelHeaderButtonAction(
<Icon/>,
(channel: Channel) => {
Expand All @@ -19,6 +21,8 @@ class PluginClass {
'Start Jitsi Meeting'
);
registry.registerPostTypeComponent('custom_jitsi', PostTypeJitsi);
registry.registerWebSocketEventHandler('custom_jitsi_config_update', () => store.dispatch(loadConfig()));
store.dispatch(loadConfig());
}
}

Expand Down

0 comments on commit a8e1cdf

Please sign in to comment.