diff --git a/go.mod b/go.mod index bb4f5138d..b521891fa 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( require ( github.com/Masterminds/semver v1.5.0 - github.com/mattermost/calls-offloader v0.3.0 - github.com/mattermost/calls-recorder v0.4.0 + github.com/mattermost/calls-offloader v0.3.2 + github.com/mattermost/calls-recorder v0.4.2 github.com/mattermost/logr/v2 v2.0.16 github.com/mattermost/mattermost/server/public v0.0.0-20230613002302-62a3ee8adcb5 github.com/mattermost/mattermost/server/v8 v8.0.0-20230622213803-fece5d5dd276 @@ -92,7 +92,7 @@ require ( golang.org/x/crypto v0.9.0 // indirect golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.54.0 // indirect diff --git a/go.sum b/go.sum index 022b0498f..4a548f61b 100644 --- a/go.sum +++ b/go.sum @@ -289,10 +289,10 @@ github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mattermost/calls-offloader v0.3.0 h1:9ygAcP/HkKH805+8nxOYhhxH3Y8SXkS+i0p+LwWzju8= -github.com/mattermost/calls-offloader v0.3.0/go.mod h1:X7T6UBd3BCnHsOYCmkg4JH7FnyDI0qupQaGxntPChNY= -github.com/mattermost/calls-recorder v0.4.0 h1:SbfS8vmCt8Re6lTgL/PO+9LovqZJw88DmHQeDHfvzgI= -github.com/mattermost/calls-recorder v0.4.0/go.mod h1:NvaEbw9B8oV2znZESocJfJ01LSKR88Ta/XCqx+U4UWI= +github.com/mattermost/calls-offloader v0.3.2 h1:Rz9pwW7XeFNcZ+/xE5wL+XEn02DCGBwcDXyymJ9O6Qc= +github.com/mattermost/calls-offloader v0.3.2/go.mod h1:RGhTfUjmZ/xtZog2xwdmPZzx+w8X8J+CPerkesR/6P0= +github.com/mattermost/calls-recorder v0.4.2 h1:cxNbgNMinSJX5ZcMZzX25LqPV/mXaWAAHOXCL4Fw72k= +github.com/mattermost/calls-recorder v0.4.2/go.mod h1:i50s8P7Wj6XWWfRnXuRcnV5bKx/ckXVD3iQk/5mR8xA= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= github.com/mattermost/ldap v3.0.4+incompatible h1:SOeNnz+JNR+foQ3yHkYqijb9MLPhXN2BZP/PdX23VDU= @@ -790,8 +790,9 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/plugin.json b/plugin.json index bd0125501..e4665e633 100644 --- a/plugin.json +++ b/plugin.json @@ -198,7 +198,7 @@ }, "props": { "min_rtcd_version": "v0.10.1", - "min_offloader_version": "v0.3.0", - "calls_recorder_version": "v0.4.0" + "min_offloader_version": "v0.3.2", + "calls_recorder_version": "v0.4.2" } } diff --git a/server/job_service.go b/server/job_service.go index 6445bf4ef..3c82696fe 100644 --- a/server/job_service.go +++ b/server/job_service.go @@ -277,7 +277,7 @@ func (s *jobService) UpdateJobRunner(runner string) error { }) } -func (s *jobService) RunRecordingJob(callID, postID, authToken string) (string, error) { +func (s *jobService) RunRecordingJob(callID, postID, recordingID, authToken string) (string, error) { cfg := s.ctx.getConfiguration() if cfg == nil { return "", fmt.Errorf("failed to get plugin configuration") @@ -302,6 +302,7 @@ func (s *jobService) RunRecordingJob(callID, postID, authToken string) (string, baseRecorderCfg.SiteURL = siteURL baseRecorderCfg.CallID = callID baseRecorderCfg.ThreadID = postID + baseRecorderCfg.RecordingID = recordingID baseRecorderCfg.AuthToken = authToken jobCfg := job.Config{ diff --git a/server/recording_api.go b/server/recording_api.go index 85417102b..9bca322dd 100644 --- a/server/recording_api.go +++ b/server/recording_api.go @@ -104,7 +104,7 @@ func (p *Plugin) handleRecordingStartAction(state *channelState, callID, userID // We don't want to keep the lock while making the API call to the service since it // could take a while to return. We lock again as soon as this returns. p.unlockCall(callID) - recJobID, jobErr := p.getJobService().RunRecordingJob(callID, state.Call.PostID, p.botSession.Token) + recJobID, jobErr := p.getJobService().RunRecordingJob(callID, state.Call.PostID, recState.ID, p.botSession.Token) state, err := p.lockCall(callID) if err != nil { res.Err = fmt.Errorf("failed to lock call: %w", err).Error() diff --git a/server/session.go b/server/session.go index 68c4b8690..7346755c4 100644 --- a/server/session.go +++ b/server/session.go @@ -71,7 +71,7 @@ func newUserSession(userID, channelID, connID string, rtc bool) *session { } } -func (p *Plugin) addUserSession(state *channelState, userID, connID, channelID string) (*channelState, error) { +func (p *Plugin) addUserSession(state *channelState, userID, connID, channelID, jobID string) (*channelState, error) { state = state.Clone() if state == nil { @@ -121,6 +121,9 @@ func (p *Plugin) addUserSession(state *channelState, userID, connID, channelID s // When the bot joins the call it means the recording has started. if userID == p.getBotID() { if state.Call.Recording != nil && state.Call.Recording.StartAt == 0 { + if state.Call.Recording.ID != jobID { + return nil, fmt.Errorf("invalid job ID for recording") + } state.Call.Recording.StartAt = time.Now().UnixMilli() state.Call.Recording.BotConnID = connID } else if state.Call.Recording == nil || state.Call.Recording.StartAt > 0 { diff --git a/server/websocket.go b/server/websocket.go index a568d0af1..e248d768b 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -39,6 +39,17 @@ const ( wsReconnectionTimeout = 10 * time.Second ) +type CallsClientJoinData struct { + ChannelID string + Title string + ThreadID string + + // JobID is the id of the job tight to the bot connection to + // a call (e.g. recording, transcription). It's a parameter reserved to the + // Calls bot only. + JobID string +} + func (p *Plugin) publishWebSocketEvent(ev string, data map[string]interface{}, broadcast *model.WebsocketBroadcast) { botID := p.getBotID() // We don't want to expose to the client that the bot is in a call. @@ -479,7 +490,8 @@ func (p *Plugin) handleLeave(us *session, userID, connID, channelID string) erro return nil } -func (p *Plugin) handleJoin(userID, connID, channelID, title, threadID string) (retErr error) { +func (p *Plugin) handleJoin(userID, connID string, joinData CallsClientJoinData) (retErr error) { + channelID := joinData.ChannelID p.LogDebug("handleJoin", "userID", userID, "connID", connID, "channelID", channelID) // We should go through only if the user has permissions to the requested channel @@ -487,6 +499,11 @@ func (p *Plugin) handleJoin(userID, connID, channelID, title, threadID string) ( if !(p.isBot(userID) || p.API.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost)) { return fmt.Errorf("forbidden") } + + if userID == p.getBotID() && joinData.JobID == "" { + return fmt.Errorf("JobID should not be empty for bot connections") + } + channel, appErr := p.API.GetChannel(channelID) if appErr != nil { return appErr @@ -495,8 +512,8 @@ func (p *Plugin) handleJoin(userID, connID, channelID, title, threadID string) ( return fmt.Errorf("cannot join call in archived channel") } - if threadID != "" { - post, appErr := p.API.GetPost(threadID) + if joinData.ThreadID != "" { + post, appErr := p.API.GetPost(joinData.ThreadID) if appErr != nil { return appErr } @@ -519,7 +536,7 @@ func (p *Plugin) handleJoin(userID, connID, channelID, title, threadID string) ( return fmt.Errorf("failed to lock call: %w", err) } - state, err := p.addUserSession(prevState, userID, connID, channel.Id) + state, err := p.addUserSession(prevState, userID, connID, channelID, joinData.JobID) if err != nil { p.unlockCall(channelID) return fmt.Errorf("failed to add user session: %w", err) @@ -541,7 +558,7 @@ func (p *Plugin) handleJoin(userID, connID, channelID, title, threadID string) ( ) } - postID, threadID, err := p.createCallStartedPost(state, userID, channelID, title, threadID) + postID, threadID, err := p.createCallStartedPost(state, userID, channelID, joinData.Title, joinData.ThreadID) if err != nil { p.LogError(err.Error()) } @@ -794,8 +811,19 @@ func (p *Plugin) WebSocketMessageHasBeenPosted(connID, userID string, req *model // it will be an empty string. threadID, _ := req.Data["threadID"].(string) + // JobID is optional, so if it's not present, + // it will be an empty string. + jobID, _ := req.Data["jobID"].(string) + + joinData := CallsClientJoinData{ + channelID, + title, + threadID, + jobID, + } + go func() { - if err := p.handleJoin(userID, connID, channelID, title, threadID); err != nil { + if err := p.handleJoin(userID, connID, joinData); err != nil { p.LogWarn(err.Error(), "userID", userID, "connID", connID, "channelID", channelID) p.publishWebSocketEvent(wsEventError, map[string]interface{}{ "data": err.Error(), diff --git a/standalone/src/common.ts b/standalone/src/common.ts index 4f88db123..a7316098c 100644 --- a/standalone/src/common.ts +++ b/standalone/src/common.ts @@ -13,6 +13,11 @@ export function getRootID() { return params.get('root_id') || ''; } +export function getJobID() { + const params = new URLSearchParams(window.location.search); + return params.get('job_id') || ''; +} + export function getToken() { if (!window.location.hash) { return ''; diff --git a/standalone/src/init.ts b/standalone/src/init.ts index 1ff94d22d..eb53ed3fa 100644 --- a/standalone/src/init.ts +++ b/standalone/src/init.ts @@ -69,13 +69,14 @@ import { handleUserDismissedNotification, } from 'plugin/websocket_handlers'; import {Reducer} from 'redux'; -import {CallActions, CurrentCallData, CurrentCallDataDefault, CallsClientConfig} from 'src/types/types'; +import {CallActions, CurrentCallData, CurrentCallDataDefault, CallsClientConfig, CallsClientJoinData} from 'src/types/types'; import { getCallID, getCallTitle, getToken, getRootID, + getJobID, } from './common'; import {applyTheme} from './theme_utils'; @@ -96,9 +97,7 @@ function setBasename() { } function connectCall( - channelID: string, - callTitle: string, - rootID: string, + joinData: CallsClientJoinData, clientConfig: CallsClientConfig, wsEventHandler: (ev: WebSocketMessage) => void, closeCb?: (err?: Error) => void, @@ -118,7 +117,7 @@ function connectCall( } }); - window.callsClient.init(channelID, callTitle, rootID).then(() => { + window.callsClient.init(joinData).then(() => { window.callsClient?.ws?.on('event', wsEventHandler); }).catch((err: Error) => { logErr(err); @@ -236,8 +235,12 @@ export default async function init(cfg: InitConfig) { return; } - const callTitle = getCallTitle(); - const rootID = getRootID(); + const joinData = { + channelID, + title: getCallTitle(), + threadID: getRootID(), + jobID: getJobID(), + }; // Setting the base URL if present, in case MM is running under a subpath. if (window.basename) { @@ -289,7 +292,7 @@ export default async function init(cfg: InitConfig) { simulcast: callsConfig(store.getState()).EnableSimulcast, }; - connectCall(channelID, callTitle, rootID, clientConfig, (ev) => { + connectCall(joinData, clientConfig, (ev) => { switch (ev.event) { case 'hello': store.dispatch(setServerVersion((ev.data as HelloData).server_version)); diff --git a/webapp/src/client.ts b/webapp/src/client.ts index cf6ab024d..7e4b3ef55 100644 --- a/webapp/src/client.ts +++ b/webapp/src/client.ts @@ -8,7 +8,7 @@ import {EventEmitter} from 'events'; // @ts-ignore import {deflate} from 'pako/lib/deflate'; -import {AudioDevices, CallsClientConfig, CallsClientStats, TrackInfo} from 'src/types/types'; +import {AudioDevices, CallsClientConfig, CallsClientStats, TrackInfo, CallsClientJoinData} from 'src/types/types'; import {logErr, logDebug, logWarn, logInfo} from './log'; import {getScreenStream} from './utils'; @@ -154,8 +154,8 @@ export default class CallsClient extends EventEmitter { } } - public async init(channelID: string, title?: string, rootId?: string) { - this.channelID = channelID; + public async init(joinData: CallsClientJoinData) { + this.channelID = joinData.channelID; if (!window.isSecureContext) { throw insecureContextErr; @@ -201,17 +201,13 @@ export default class CallsClient extends EventEmitter { if (isReconnect) { logDebug('ws reconnect, sending reconnect msg'); ws.send('reconnect', { - channelID, + channelID: joinData.channelID, originalConnID, prevConnID, }); } else { logDebug('ws open, sending join msg'); - ws.send('join', { - channelID, - title, - threadID: rootId, - }); + ws.send('join', joinData); } }); diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 098af51c9..29d7a0df4 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -514,7 +514,11 @@ export default class Plugin { }); }); - window.callsClient.init(channelID, title, rootId).catch((err: Error) => { + window.callsClient.init({ + channelID, + title, + threadID: rootId, + }).catch((err: Error) => { logErr(err); unmountCallWidget(); store.dispatch(displayCallErrorModal(channelID, err)); diff --git a/webapp/src/types/types.ts b/webapp/src/types/types.ts index bb1906f66..aaa42da27 100644 --- a/webapp/src/types/types.ts +++ b/webapp/src/types/types.ts @@ -23,6 +23,17 @@ export type ChannelState = { enabled?: boolean; } +export type CallsClientJoinData = { + channelID: string; + title?: string; + threadID?: string; + + // Calls bot only + // jobID is the id of the job tight to the bot connection to + // a call (e.g. recording, transcription). + jobID?: string; +} + export type CallsClientConfig = { wsURL: string; authToken?: string;