Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MM-51499] Implement API endpoint to start calls #437

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 5 additions & 21 deletions e2e/tests/global_widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ import {test, expect} from '@playwright/test';

import {baseURL, defaultTeam, pluginID} from '../constants';

import {getChannelNamesForTest, getUserStoragesForTest} from '../utils';
import {getChannelNamesForTest, getUserStoragesForTest, getChannelID} from '../utils';

test.describe('global widget', () => {
test.use({storageState: getUserStoragesForTest()[0]});

test('start call', async ({page, request}) => {
const channelName = getChannelNamesForTest()[0];
const resp = await request.get(`${baseURL}/api/v4/teams/name/${defaultTeam}/channels/name/${channelName}`);
const channel = await resp.json();

await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${channel.id}`);
await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${await getChannelID(request)}`);
await expect(page.locator('#calls-widget')).toBeVisible();
await expect(page.locator('#calls-widget-leave-button')).toBeVisible();
await page.locator('#calls-widget-leave-button').click();
Expand All @@ -21,11 +17,7 @@ test.describe('global widget', () => {

test('recording widget banner', async ({page, request, context}) => {
// start call
const channelName = getChannelNamesForTest()[0];
const resp = await request.get(`${baseURL}/api/v4/teams/name/${defaultTeam}/channels/name/${channelName}`);
const channel = await resp.json();

await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${channel.id}`);
await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${await getChannelID(request)}`);
await expect(page.locator('#calls-widget')).toBeVisible();

// open popout to control recording
Expand Down Expand Up @@ -65,11 +57,7 @@ test.describe('global widget', () => {
context,
}) => {
// start call
const channelName = getChannelNamesForTest()[0];
const resp = await request.get(`${baseURL}/api/v4/teams/name/${defaultTeam}/channels/name/${channelName}`);
const channel = await resp.json();

await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${channel.id}`);
await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${await getChannelID(request)}`);
await expect(page.locator('#calls-widget')).toBeVisible();

// open popout to control recording
Expand Down Expand Up @@ -138,11 +126,7 @@ test.describe('global widget', () => {
context,
}) => {
// start call
const channelName = getChannelNamesForTest()[0];
const resp = await request.get(`${baseURL}/api/v4/teams/name/${defaultTeam}/channels/name/${channelName}`);
const channel = await resp.json();

await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${channel.id}`);
await page.goto(`${baseURL}/plugins/${pluginID}/standalone/widget.html?call_id=${await getChannelID(request)}`);
await expect(page.locator('#calls-widget')).toBeVisible();

// open popout to control recording
Expand Down
25 changes: 23 additions & 2 deletions e2e/tests/start_call.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import {readFile} from 'fs/promises';
import {test, expect, chromium} from '@playwright/test';

import PlaywrightDevPage from '../page';
import {getChannelNamesForTest, getUsernamesForTest, getUserStoragesForTest} from '../utils';
import {getChannelNamesForTest, getUsernamesForTest, getUserStoragesForTest, getChannelID} from '../utils';

import {adminState} from '../constants';
import {adminState, baseURL, defaultTeam, pluginID} from '../constants';

const userStorages = getUserStoragesForTest();
const usernames = getUsernamesForTest();
Expand Down Expand Up @@ -54,6 +54,27 @@ test.describe('start/join call in channel with calls disabled', () => {
test.describe('start new call', () => {
test.use({storageState: userStorages[0]});

test('API endpoint', async ({page, request}) => {
const channelID = await getChannelID(request);

await request.post(`${baseURL}/plugins/${pluginID}/calls/start`, {
data: {
channel_id: channelID,
},
headers: {'X-Requested-With': 'XMLHttpRequest'},
});

// verify the call post is created.
await expect(page.getByTestId('call-thread').filter({has: page.getByText(`${usernames[0]} started a call`)})).toBeVisible();

await request.post(`${baseURL}/plugins/${pluginID}/calls/${channelID}/end`, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
});

// verify the call has ended.
await expect(page.getByTestId('call-thread').filter({has: page.getByText(`${usernames[0]} started a call`)})).toBeHidden();
});

test('channel header button', async ({page}) => {
const devPage = new PlaywrightDevPage(page);
await devPage.startCall();
Expand Down
1 change: 0 additions & 1 deletion e2e/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@ export type UserState = {
password: string;
storageStatePath: string;
};

10 changes: 8 additions & 2 deletions e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {chromium} from '@playwright/test';
import {chromium, APIRequestContext} from '@playwright/test';

import PlaywrightDevPage from './page';

import {userPrefix, channelPrefix} from './constants';
import {userPrefix, channelPrefix, baseURL, defaultTeam} from './constants';

export function getChannelNamesForTest() {
let idx = 0;
Expand Down Expand Up @@ -38,3 +38,9 @@ export async function startCall(userState: string) {
return userPage;
}

export async function getChannelID(request: APIRequestContext, channelIdx?: number) {
const channelName = getChannelNamesForTest()[channelIdx || 0];
const resp = await request.get(`${baseURL}/api/v4/teams/name/${defaultTeam}/channels/name/${channelName}`);
const channel = await resp.json();
return channel.id;
}
89 changes: 22 additions & 67 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"path/filepath"
"regexp"
"strings"
"time"

"golang.org/x/time/rate"

Expand All @@ -23,6 +22,7 @@ import (

var chRE = regexp.MustCompile(`^\/([a-z0-9]+)$`)
var callEndRE = regexp.MustCompile(`^\/calls\/([a-z0-9]+)\/end$`)
var callStartRE = regexp.MustCompile(`^\/calls\/start$`)

const requestBodyMaxSizeBytes = 1024 * 1024 // 1MB

Expand Down Expand Up @@ -170,92 +170,42 @@ func (p *Plugin) handleGetAllChannels(w http.ResponseWriter, r *http.Request) {
}
}

func (p *Plugin) handleEndCall(w http.ResponseWriter, r *http.Request, channelID string) {
func (p *Plugin) handleStartCall(w http.ResponseWriter, r *http.Request) {
var res httpResponse
defer p.httpAudit("handleEndCall", &res, w, r)
defer p.httpAudit("handleStartCall", &res, w, r)

userID := r.Header.Get("Mattermost-User-Id")

isAdmin := p.API.HasPermissionTo(userID, model.PermissionManageSystem)

state, err := p.kvGetChannelState(channelID)
if err != nil {
var data CallStartRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, requestBodyMaxSizeBytes)).Decode(&data); err != nil {
res.Err = err.Error()
res.Code = http.StatusInternalServerError
return
}

if state == nil || state.Call == nil {
res.Err = "no call ongoing"
res.Code = http.StatusBadRequest
return
}

if !isAdmin && state.Call.OwnerID != userID {
res.Err = "no permissions to end the call"
// TODO: consider forwarding proper error codes.
if err := p.startCall(userID, data); err != nil {
res.Err = err.Error()
res.Code = http.StatusForbidden
return
}

callID := state.Call.ID

if err := p.kvSetAtomicChannelState(channelID, func(state *channelState) (*channelState, error) {
if state == nil || state.Call == nil {
return nil, nil
}
res.Code = http.StatusOK
res.Msg = "success"
}

if state.Call.ID != callID {
return nil, fmt.Errorf("previous call has ended and new one has started")
}
func (p *Plugin) handleEndCall(w http.ResponseWriter, r *http.Request, channelID string) {
var res httpResponse
defer p.httpAudit("handleEndCall", &res, w, r)

if state.Call.EndAt == 0 {
state.Call.EndAt = time.Now().UnixMilli()
}
userID := r.Header.Get("Mattermost-User-Id")

return state, nil
}); err != nil {
res.Err = err.Error()
if err := p.endCall(userID, channelID); err != nil {
res.Code = http.StatusForbidden
res.Msg = err.Error()
return
}

p.publishWebSocketEvent(wsEventCallEnd, map[string]interface{}{}, &model.WebsocketBroadcast{ChannelId: channelID, ReliableClusterSend: true})

go func() {
// We wait a few seconds for the call to end cleanly. If this doesn't
// happen we force end it.
time.Sleep(5 * time.Second)

state, err := p.kvGetChannelState(channelID)
if err != nil {
p.LogError(err.Error())
return
}
if state == nil || state.Call == nil || state.Call.ID != callID {
return
}

p.LogInfo("call state is still in store, force ending it", "channelID", channelID)

if state.Call.Recording != nil && state.Call.Recording.EndAt == 0 {
p.LogInfo("recording is in progress, force ending it", "channelID", channelID, "jobID", state.Call.Recording.JobID)

if err := p.jobService.StopJob(state.Call.Recording.JobID); err != nil {
p.LogError("failed to stop recording job", "error", err.Error(), "channelID", channelID, "jobID", state.Call.Recording.JobID)
}
}

for connID := range state.Call.Sessions {
if err := p.closeRTCSession(userID, connID, channelID, state.NodeID); err != nil {
p.LogError(err.Error())
}
}

if err := p.cleanCallState(channelID); err != nil {
p.LogError(err.Error())
}
}()

res.Code = http.StatusOK
res.Msg = "success"
}
Expand Down Expand Up @@ -564,6 +514,11 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
return
}

if callStartRE.MatchString(r.URL.Path) {
p.handleStartCall(w, r)
return
}

if r.URL.Path == "/telemetry/track" {
p.handleTrackEvent(w, r)
return
Expand Down
Loading