mirror of
https://github.com/nextcloud/spreed.git
synced 2025-12-18 05:20:50 +01:00
Merge pull request #8720 from nextcloud/change-recording-status-when-notified-by-recording-server
Change recording status when notified by recording server
This commit is contained in:
commit
822e1898ee
32 changed files with 1351 additions and 75 deletions
|
|
@ -34,6 +34,8 @@ return [
|
|||
['name' => 'Recording#getWelcomeMessage', 'url' => '/api/{apiVersion}/recording/welcome/{serverId}', 'verb' => 'GET', 'requirements' => array_merge($requirements, [
|
||||
'serverId' => '\d+',
|
||||
])],
|
||||
/** @see \OCA\Talk\Controller\RecordingController::backend() */
|
||||
['name' => 'Recording#backend', 'url' => '/api/{apiVersion}/recording/backend', 'verb' => 'POST', 'requirements' => $requirements],
|
||||
/** @see \OCA\Talk\Controller\RecordingController::start() */
|
||||
['name' => 'Recording#start', 'url' => '/api/{apiVersion}/recording/{token}', 'verb' => 'POST', 'requirements' => $requirements],
|
||||
/** @see \OCA\Talk\Controller\RecordingController::stop() */
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ title: Constants
|
|||
* `0` - No recording
|
||||
* `1` - Recording video
|
||||
* `2` - Recording audio
|
||||
* `3` - Starting video recording
|
||||
* `4` - Starting audio recording
|
||||
* `5` - Recording failed
|
||||
|
||||
## Chat
|
||||
|
||||
|
|
|
|||
|
|
@ -110,3 +110,88 @@
|
|||
+ `400 Bad Request` Error: `system`: Internal system error
|
||||
+ `403 Forbidden` When the user is not a moderator/owne
|
||||
+ `404 Not Found` Room not found
|
||||
|
||||
## Recording server requests
|
||||
|
||||
* Required capability: `recording-v1`
|
||||
* Method: `POST`
|
||||
* Endpoint: `/recording/backend`
|
||||
|
||||
* Header:
|
||||
|
||||
| field | type | Description |
|
||||
| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `TALK_RECORDING_RANDOM` | string | Random string that needs to be concatenated with request body to generate the checksum using the secret configured for the backend. |
|
||||
| `TALK_RECORDING_CHECKSUM` | string | The checksum generated with `TALK_RECORDING_RANDOM`. |
|
||||
|
||||
* Data:
|
||||
|
||||
- Body as a JSON encoded string; format depends on the request type, see below.
|
||||
|
||||
* Response:
|
||||
- Status code:
|
||||
+ `200 OK`
|
||||
+ `400 Bad Request`: When the body data does not match the expected format.
|
||||
+ `403 Forbidden`: When the request validation failed.
|
||||
|
||||
### Started call recording
|
||||
|
||||
* Data format:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "started",
|
||||
"started": {
|
||||
"token": "the-token-of-the-room",
|
||||
"status": "the-type-of-recording (see [Constants - Call recording status](constants.md#call-recording-status))",
|
||||
"actor": {
|
||||
"type": "the-type-of-the-actor",
|
||||
"id": "the-id-of-the-actor",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
* Response:
|
||||
- (Additional) Status code:
|
||||
+ `404 Not Found`: When the room is not found.
|
||||
|
||||
### Stopped call recording
|
||||
|
||||
* Data format:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "stopped",
|
||||
"stopped": {
|
||||
"token": "the-token-of-the-room",
|
||||
"actor": {
|
||||
"type": "the-type-of-the-actor",
|
||||
"id": "the-id-of-the-actor",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `actor` is optional
|
||||
|
||||
* Response:
|
||||
- (Additional) Status code:
|
||||
+ `404 Not Found`: When the room is not found.
|
||||
|
||||
### Failed call recording
|
||||
|
||||
* Data format:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "failed",
|
||||
"failed": {
|
||||
"token": "the-token-of-the-room",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
* Response:
|
||||
- (Additional) Status code:
|
||||
+ `404 Not Found`: When the room is not found.
|
||||
|
|
|
|||
|
|
@ -150,8 +150,8 @@ class Listener {
|
|||
return false;
|
||||
}
|
||||
|
||||
if ($room->getCallRecording() !== Room::RECORDING_NONE) {
|
||||
$this->recordingService->stop($room);
|
||||
if ($room->getCallRecording() !== Room::RECORDING_NONE && $room->getCallRecording() !== Room::RECORDING_FAILED) {
|
||||
$this->recordingService->stop($room, $actor);
|
||||
}
|
||||
if ($actor instanceof Participant) {
|
||||
$actorId = $actor->getAttendee()->getActorId();
|
||||
|
|
|
|||
|
|
@ -544,6 +544,8 @@ class SystemMessage {
|
|||
if ($currentUserIsActor) {
|
||||
$parsedMessage = $this->l->t('You stopped the audio recording');
|
||||
}
|
||||
} elseif ($message === 'recording_failed') {
|
||||
$parsedMessage = $this->l->t('The recording failed');
|
||||
} elseif ($message === 'poll_voted') {
|
||||
$parsedParameters['poll'] = $parameters['poll'];
|
||||
$parsedParameters['poll']['id'] = (string) $parsedParameters['poll']['id'];
|
||||
|
|
|
|||
|
|
@ -501,12 +501,23 @@ class Listener implements IEventListener {
|
|||
}
|
||||
|
||||
public static function setCallRecording(ModifyRoomEvent $event): void {
|
||||
$recordingHasStarted = in_array($event->getOldValue(), [Room::RECORDING_NONE, Room::RECORDING_VIDEO_STARTING, Room::RECORDING_AUDIO_STARTING, Room::RECORDING_FAILED])
|
||||
&& in_array($event->getNewValue(), [Room::RECORDING_VIDEO, Room::RECORDING_AUDIO]);
|
||||
$recordingHasStopped = in_array($event->getOldValue(), [Room::RECORDING_VIDEO, Room::RECORDING_AUDIO])
|
||||
&& $event->getNewValue() === Room::RECORDING_NONE;
|
||||
$recordingHasFailed = in_array($event->getOldValue(), [Room::RECORDING_VIDEO, Room::RECORDING_AUDIO])
|
||||
&& $event->getNewValue() === Room::RECORDING_FAILED;
|
||||
|
||||
if (!$recordingHasStarted && !$recordingHasStopped && !$recordingHasFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$prefix = self::getCallRecordingPrefix($event);
|
||||
$suffix = self::getCallRecordingSuffix($event);
|
||||
$systemMessage = $prefix . 'recording_' . $suffix;
|
||||
|
||||
$listener = Server::get(self::class);
|
||||
$listener->sendSystemMessage($event->getRoom(), $systemMessage);
|
||||
$listener->sendSystemMessage($event->getRoom(), $systemMessage, [], $event->getActor());
|
||||
}
|
||||
|
||||
private static function getCallRecordingSuffix(ModifyRoomEvent $event): string {
|
||||
|
|
@ -515,15 +526,20 @@ class Listener implements IEventListener {
|
|||
Room::RECORDING_VIDEO,
|
||||
Room::RECORDING_AUDIO,
|
||||
];
|
||||
$suffix = in_array($newStatus, $startStatus) ? 'started' : 'stopped';
|
||||
return $suffix;
|
||||
if (in_array($newStatus, $startStatus)) {
|
||||
return 'started';
|
||||
}
|
||||
if ($newStatus === Room::RECORDING_FAILED) {
|
||||
return 'failed';
|
||||
}
|
||||
return 'stopped';
|
||||
}
|
||||
|
||||
private static function getCallRecordingPrefix(ModifyRoomEvent $event): string {
|
||||
$newValue = $event->getNewValue();
|
||||
$oldValue = $event->getOldValue();
|
||||
$isAudioStatus = $newValue === Room::RECORDING_AUDIO
|
||||
|| $oldValue === Room::RECORDING_AUDIO;
|
||||
|| ($oldValue === Room::RECORDING_AUDIO && $newValue !== Room::RECORDING_FAILED);
|
||||
return $isAudioStatus ? 'audio_' : '';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,13 @@ namespace OCA\Talk\Controller;
|
|||
use InvalidArgumentException;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Exceptions\ParticipantNotFoundException;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\RecordingService;
|
||||
use OCA\Talk\Service\RoomService;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\Http\Client\IClientService;
|
||||
|
|
@ -42,7 +48,10 @@ class RecordingController extends AEnvironmentAwareController {
|
|||
private ?string $userId,
|
||||
private Config $talkConfig,
|
||||
private IClientService $clientService,
|
||||
private Manager $manager,
|
||||
private ParticipantService $participantService,
|
||||
private RecordingService $recordingService,
|
||||
private RoomService $roomService,
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
|
@ -109,13 +118,158 @@ class RecordingController extends AEnvironmentAwareController {
|
|||
return hash_equals($hash, strtolower($checksum));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the body of the backend request. This can be overridden in
|
||||
* tests.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getInputStream(): string {
|
||||
return file_get_contents('php://input');
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend API to update recording status by backends.
|
||||
*
|
||||
* @PublicPage
|
||||
* @BruteForceProtection(action=talkRecordingSecret)
|
||||
*
|
||||
* @return DataResponse
|
||||
*/
|
||||
public function backend(): DataResponse {
|
||||
$json = $this->getInputStream();
|
||||
if (!$this->validateBackendRequest($json)) {
|
||||
$response = new DataResponse([
|
||||
'type' => 'error',
|
||||
'error' => [
|
||||
'code' => 'invalid_request',
|
||||
'message' => 'The request could not be authenticated.',
|
||||
],
|
||||
], Http::STATUS_FORBIDDEN);
|
||||
$response->throttle();
|
||||
return $response;
|
||||
}
|
||||
|
||||
$message = json_decode($json, true);
|
||||
switch ($message['type'] ?? '') {
|
||||
case 'started':
|
||||
return $this->backendStarted($message['started']);
|
||||
case 'stopped':
|
||||
return $this->backendStopped($message['stopped']);
|
||||
case 'failed':
|
||||
return $this->backendFailed($message['failed']);
|
||||
default:
|
||||
return new DataResponse([
|
||||
'type' => 'error',
|
||||
'error' => [
|
||||
'code' => 'unknown_type',
|
||||
'message' => 'The given type ' . json_encode($message) . ' is not supported.',
|
||||
],
|
||||
], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private function backendStarted(array $started): DataResponse {
|
||||
$token = $started['token'];
|
||||
$status = $started['status'];
|
||||
$actor = $started['actor'];
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$this->logger->debug('Failed to get room {token}', [
|
||||
'token' => $token,
|
||||
'app' => 'spreed-recording',
|
||||
]);
|
||||
return new DataResponse([
|
||||
'type' => 'error',
|
||||
'error' => [
|
||||
'code' => 'no_such_room',
|
||||
'message' => 'Room not found.',
|
||||
],
|
||||
], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$participant = $this->participantService->getParticipantByActor($room, $actor['type'], $actor['id']);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
$participant = null;
|
||||
}
|
||||
|
||||
$this->roomService->setCallRecording($room, $status, $participant);
|
||||
|
||||
return new DataResponse();
|
||||
}
|
||||
|
||||
private function backendStopped(array $stopped): DataResponse {
|
||||
$token = $stopped['token'];
|
||||
$actor = null;
|
||||
if (array_key_exists('actor', $stopped)) {
|
||||
$actor = $stopped['actor'];
|
||||
}
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$this->logger->debug('Failed to get room {token}', [
|
||||
'token' => $token,
|
||||
'app' => 'spreed-recording',
|
||||
]);
|
||||
return new DataResponse([
|
||||
'type' => 'error',
|
||||
'error' => [
|
||||
'code' => 'no_such_room',
|
||||
'message' => 'Room not found.',
|
||||
],
|
||||
], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($actor === null) {
|
||||
throw new ParticipantNotFoundException();
|
||||
}
|
||||
|
||||
$participant = $this->participantService->getParticipantByActor($room, $actor['type'], $actor['id']);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
$participant = null;
|
||||
}
|
||||
|
||||
$this->roomService->setCallRecording($room, Room::RECORDING_NONE, $participant);
|
||||
|
||||
return new DataResponse();
|
||||
}
|
||||
|
||||
private function backendFailed(array $failed): DataResponse {
|
||||
$token = $failed['token'];
|
||||
|
||||
try {
|
||||
$room = $this->manager->getRoomByToken($token);
|
||||
} catch (RoomNotFoundException $e) {
|
||||
$this->logger->debug('Failed to get room {token}', [
|
||||
'token' => $token,
|
||||
'app' => 'spreed-recording',
|
||||
]);
|
||||
return new DataResponse([
|
||||
'type' => 'error',
|
||||
'error' => [
|
||||
'code' => 'no_such_room',
|
||||
'message' => 'Room not found.',
|
||||
],
|
||||
], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
$this->roomService->setCallRecording($room, Room::RECORDING_FAILED);
|
||||
|
||||
return new DataResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @RequireLoggedInModeratorParticipant
|
||||
*/
|
||||
public function start(int $status): DataResponse {
|
||||
try {
|
||||
$this->recordingService->start($this->room, $status, $this->userId);
|
||||
$this->recordingService->start($this->room, $status, $this->userId, $this->participant);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
|
@ -128,7 +282,7 @@ class RecordingController extends AEnvironmentAwareController {
|
|||
*/
|
||||
public function stop(): DataResponse {
|
||||
try {
|
||||
$this->recordingService->stop($this->room);
|
||||
$this->recordingService->stop($this->room, $this->participant);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
|
|
|||
27
lib/Exceptions/RecordingNotFoundException.php
Normal file
27
lib/Exceptions/RecordingNotFoundException.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Exceptions;
|
||||
|
||||
class RecordingNotFoundException extends \Exception {
|
||||
}
|
||||
|
|
@ -24,10 +24,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Talk\Recording;
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Exceptions\RecordingNotFoundException;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Security\ISecureRandom;
|
||||
|
|
@ -136,13 +140,17 @@ class BackendNotifier {
|
|||
$this->doRequest($url, $params);
|
||||
}
|
||||
|
||||
public function start(Room $room, int $status, string $owner): void {
|
||||
public function start(Room $room, int $status, string $owner, Participant $participant): void {
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'start',
|
||||
'start' => [
|
||||
'status' => $status,
|
||||
'owner' => $owner,
|
||||
'actor' => [
|
||||
'type' => $participant->getAttendee()->getActorType(),
|
||||
'id' => $participant->getAttendee()->getActorId(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
|
|
@ -153,11 +161,28 @@ class BackendNotifier {
|
|||
]);
|
||||
}
|
||||
|
||||
public function stop(Room $room): void {
|
||||
public function stop(Room $room, ?Participant $participant = null): void {
|
||||
$parameters = [];
|
||||
if ($participant !== null) {
|
||||
$parameters['actor'] = [
|
||||
'type' => $participant->getAttendee()->getActorType(),
|
||||
'id' => $participant->getAttendee()->getActorId(),
|
||||
];
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'stop',
|
||||
]);
|
||||
try {
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'stop',
|
||||
'stop' => $parameters,
|
||||
]);
|
||||
} catch (ClientException $e) {
|
||||
if ($e->getResponse()->getStatusCode() === Http::STATUS_NOT_FOUND) {
|
||||
throw new RecordingNotFoundException();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Send stop message: {token} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ class Room {
|
|||
public const RECORDING_NONE = 0;
|
||||
public const RECORDING_VIDEO = 1;
|
||||
public const RECORDING_AUDIO = 2;
|
||||
public const RECORDING_VIDEO_STARTING = 3;
|
||||
public const RECORDING_AUDIO_STARTING = 4;
|
||||
public const RECORDING_FAILED = 5;
|
||||
|
||||
/** @deprecated Use self::TYPE_UNKNOWN */
|
||||
public const UNKNOWN_CALL = self::TYPE_UNKNOWN;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ use OC\User\NoUserException;
|
|||
use OCA\Talk\Chat\ChatManager;
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Exceptions\ParticipantNotFoundException;
|
||||
use OCA\Talk\Exceptions\RecordingNotFoundException;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Recording\BackendNotifier;
|
||||
|
|
@ -70,31 +71,40 @@ class RecordingService {
|
|||
) {
|
||||
}
|
||||
|
||||
public function start(Room $room, int $status, string $owner): void {
|
||||
public function start(Room $room, int $status, string $owner, Participant $participant): void {
|
||||
$availableRecordingTypes = [Room::RECORDING_VIDEO, Room::RECORDING_AUDIO];
|
||||
if (!in_array($status, $availableRecordingTypes)) {
|
||||
throw new InvalidArgumentException('status');
|
||||
}
|
||||
if ($room->getCallRecording() !== Room::RECORDING_NONE) {
|
||||
if ($room->getCallRecording() !== Room::RECORDING_NONE && $room->getCallRecording() !== Room::RECORDING_FAILED) {
|
||||
throw new InvalidArgumentException('recording');
|
||||
}
|
||||
if (!$room->getActiveSince() instanceof \DateTimeInterface) {
|
||||
throw new InvalidArgumentException('call');
|
||||
}
|
||||
if (!$this->config->isRecordingEnabled()) {
|
||||
throw new InvalidArgumentException('config');
|
||||
}
|
||||
|
||||
$this->backendNotifier->start($room, $status, $owner);
|
||||
$this->backendNotifier->start($room, $status, $owner, $participant);
|
||||
|
||||
$this->roomService->setCallRecording($room, $status);
|
||||
$startingStatus = $status == Room::RECORDING_VIDEO ? Room::RECORDING_VIDEO_STARTING : Room::RECORDING_AUDIO_STARTING;
|
||||
$this->roomService->setCallRecording($room, $startingStatus);
|
||||
}
|
||||
|
||||
public function stop(Room $room): void {
|
||||
public function stop(Room $room, ?Participant $participant = null): void {
|
||||
if ($room->getCallRecording() === Room::RECORDING_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->backendNotifier->stop($room);
|
||||
|
||||
$this->roomService->setCallRecording($room);
|
||||
try {
|
||||
$this->backendNotifier->stop($room, $participant);
|
||||
} catch (RecordingNotFoundException $e) {
|
||||
// If the recording to be stopped is not known to the recording
|
||||
// server it will never notify that the recording was stopped, so
|
||||
// the status needs to be explicitly changed here.
|
||||
$this->roomService->setCallRecording($room, Room::RECORDING_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Room $room, string $owner, array $file): void {
|
||||
|
|
|
|||
|
|
@ -369,21 +369,23 @@ class RoomService {
|
|||
/**
|
||||
* @param Room $room
|
||||
* @param integer $status 0 none|1 video|2 audio
|
||||
* @param Participant|null $participant the Participant that changed the
|
||||
* state, null for the current user
|
||||
* @throws \InvalidArgumentException When the status is invalid, not Room::RECORDING_*
|
||||
* @throws \InvalidArgumentException When trying to start
|
||||
*/
|
||||
public function setCallRecording(Room $room, int $status = Room::RECORDING_NONE): void {
|
||||
public function setCallRecording(Room $room, int $status = Room::RECORDING_NONE, ?Participant $participant = null): void {
|
||||
if (!$this->config->isRecordingEnabled() && $status !== Room::RECORDING_NONE) {
|
||||
throw new InvalidArgumentException('config');
|
||||
}
|
||||
|
||||
$availableRecordingStatus = [Room::RECORDING_NONE, Room::RECORDING_VIDEO, Room::RECORDING_AUDIO];
|
||||
$availableRecordingStatus = [Room::RECORDING_NONE, Room::RECORDING_VIDEO, Room::RECORDING_AUDIO, Room::RECORDING_VIDEO_STARTING, Room::RECORDING_AUDIO_STARTING, Room::RECORDING_FAILED];
|
||||
if (!in_array($status, $availableRecordingStatus)) {
|
||||
throw new InvalidArgumentException('status');
|
||||
}
|
||||
|
||||
$oldStatus = $room->getCallRecording();
|
||||
$event = new ModifyRoomEvent($room, 'callRecording', $status, $oldStatus);
|
||||
$event = new ModifyRoomEvent($room, 'callRecording', $status, $oldStatus, $participant);
|
||||
$this->dispatcher->dispatch(Room::EVENT_BEFORE_SET_CALL_RECORDING, $event);
|
||||
|
||||
$update = $this->db->getQueryBuilder();
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@
|
|||
<file name="tests/stubs/oc_comments_comment.php" />
|
||||
<file name="tests/stubs/oc_comments_manager.php" />
|
||||
<file name="tests/stubs/oc_hooks_emitter.php" />
|
||||
<file name="tests/stubs/GuzzleHttp_Exception_ClientException.php" />
|
||||
<file name="tests/stubs/GuzzleHttp_Exception_ConnectException.php" />
|
||||
<file name="tests/stubs/GuzzleHttp_Exception_ServerException.php" />
|
||||
<file name="tests/stubs/Symfony_Component_EventDispatcher_GenericEvent.php" />
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@
|
|||
| `TALK_RECORDING_RANDOM` | string | Random string that needs to be concatenated with request body to generate the checksum using the secret configured for the backend. |
|
||||
| `TALK_RECORDING_CHECKSUM` | string | The checksum generated with `TALK_RECORDING_RANDOM`. |
|
||||
|
||||
* Data:
|
||||
|
||||
- Body as a JSON encoded string; format depends on the request type, see below.
|
||||
|
||||
* Response:
|
||||
- Status code:
|
||||
+ `200 OK`
|
||||
|
|
@ -33,7 +37,7 @@
|
|||
|
||||
### Start call recording
|
||||
|
||||
* Data format (JSON):
|
||||
* Data format:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -41,6 +45,10 @@
|
|||
"start": {
|
||||
"status": "the-type-of-recording (1 for audio and video, 2 for audio only)",
|
||||
"owner": "the-user-to-upload-the-resulting-file-as",
|
||||
"actor": {
|
||||
"type": "the-type-of-the-actor",
|
||||
"id": "the-id-of-the-actor",
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -52,5 +60,17 @@
|
|||
```json
|
||||
{
|
||||
"type": "stop",
|
||||
"stop": {
|
||||
"actor": {
|
||||
"type": "the-type-of-the-actor",
|
||||
"id": "the-id-of-the-actor",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `actor` is optional
|
||||
|
||||
* Response:
|
||||
- (Additional) Status code:
|
||||
+ `404 Not Found`: When there is no recording for the token.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ Module to send requests to the Nextcloud server.
|
|||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
|
|
@ -74,6 +75,102 @@ def doRequest(backend, request, retries=3):
|
|||
logger.exception(f"Failed to send message to backend, giving up!")
|
||||
raise
|
||||
|
||||
def backendRequest(backend, data):
|
||||
"""
|
||||
Sends the data to the backend on the endpoint to receive notifications from
|
||||
the recording server.
|
||||
|
||||
The data is automatically wrapped in a request for the appropriate URL and
|
||||
with the needed headers.
|
||||
|
||||
:param backend: the backend to send the data to.
|
||||
:param data: the data to send.
|
||||
"""
|
||||
url = backend + '/ocs/v2.php/apps/spreed/api/v1/recording/backend'
|
||||
|
||||
data = json.dumps(data).encode()
|
||||
|
||||
random, checksum = getRandomAndChecksum(backend, data)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'OCS-ApiRequest': 'true',
|
||||
'Talk-Recording-Random': random,
|
||||
'Talk-Recording-Checksum': checksum,
|
||||
}
|
||||
|
||||
backendRequest = Request(url, data, headers)
|
||||
|
||||
doRequest(backend, backendRequest)
|
||||
|
||||
def started(backend, token, status, actorType, actorId):
|
||||
"""
|
||||
Notifies the backend that the recording was started.
|
||||
|
||||
:param backend: the backend of the conversation.
|
||||
:param token: the token of the conversation.
|
||||
:param actorType: the actor type of the Talk participant that stopped the
|
||||
recording.
|
||||
:param actorId: the actor id of the Talk participant that stopped the
|
||||
recording.
|
||||
"""
|
||||
|
||||
backendRequest(backend, {
|
||||
'type': 'started',
|
||||
'started': {
|
||||
'token': token,
|
||||
'status': status,
|
||||
'actor': {
|
||||
'type': actorType,
|
||||
'id': actorId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def stopped(backend, token, actorType, actorId):
|
||||
"""
|
||||
Notifies the backend that the recording was stopped.
|
||||
|
||||
:param backend: the backend of the conversation.
|
||||
:param token: the token of the conversation.
|
||||
:param actorType: the actor type of the Talk participant that started the
|
||||
recording.
|
||||
:param actorId: the actor id of the Talk participant that started the
|
||||
recording.
|
||||
"""
|
||||
|
||||
data = {
|
||||
'type': 'stopped',
|
||||
'stopped': {
|
||||
'token': token,
|
||||
},
|
||||
}
|
||||
|
||||
if actorType != None and actorId != None:
|
||||
data['stopped']['actor'] = {
|
||||
'type': actorType,
|
||||
'id': actorId,
|
||||
}
|
||||
|
||||
backendRequest(backend, data)
|
||||
|
||||
def failed(backend, token):
|
||||
"""
|
||||
Notifies the backend that the recording failed.
|
||||
|
||||
:param backend: the backend of the conversation.
|
||||
:param token: the token of the conversation.
|
||||
"""
|
||||
|
||||
data = {
|
||||
'type': 'failed',
|
||||
'failed': {
|
||||
'token': token,
|
||||
},
|
||||
}
|
||||
|
||||
backendRequest(backend, data)
|
||||
|
||||
def uploadRecording(backend, token, fileName, owner):
|
||||
"""
|
||||
Upload the recording specified by fileName.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import hmac
|
|||
from threading import Lock, Thread
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||
|
||||
from nextcloud.talk import recording
|
||||
from .Config import config
|
||||
|
|
@ -37,6 +37,7 @@ from .Service import RECORDING_STATUS_AUDIO_AND_VIDEO, Service
|
|||
app = Flask(__name__)
|
||||
|
||||
services = {}
|
||||
servicesStopping = {}
|
||||
servicesLock = Lock()
|
||||
|
||||
@app.route("/api/v1/welcome", methods=["GET"])
|
||||
|
|
@ -119,12 +120,24 @@ def startRecording(backend, token, data):
|
|||
if 'owner' not in data['start']:
|
||||
raise BadRequest()
|
||||
|
||||
if 'actor' not in data['start']:
|
||||
raise BadRequest()
|
||||
|
||||
if 'type' not in data['start']['actor']:
|
||||
raise BadRequest()
|
||||
|
||||
if 'id' not in data['start']['actor']:
|
||||
raise BadRequest()
|
||||
|
||||
status = RECORDING_STATUS_AUDIO_AND_VIDEO
|
||||
if 'status' in data['start']:
|
||||
status = data['start']['status']
|
||||
|
||||
owner = data['start']['owner']
|
||||
|
||||
actorType = data['start']['actor']['type']
|
||||
actorId = data['start']['actor']['id']
|
||||
|
||||
service = None
|
||||
with servicesLock:
|
||||
if serviceId in services:
|
||||
|
|
@ -137,12 +150,12 @@ def startRecording(backend, token, data):
|
|||
|
||||
app.logger.info(f"Start recording: {backend} {token}")
|
||||
|
||||
serviceStartThread = Thread(target=_startRecordingService, args=[service], daemon=True)
|
||||
serviceStartThread = Thread(target=_startRecordingService, args=[service, actorType, actorId], daemon=True)
|
||||
serviceStartThread.start()
|
||||
|
||||
return {}
|
||||
|
||||
def _startRecordingService(service):
|
||||
def _startRecordingService(service, actorType, actorId):
|
||||
"""
|
||||
Helper function to start a recording service.
|
||||
|
||||
|
|
@ -154,7 +167,7 @@ def _startRecordingService(service):
|
|||
serviceId = f'{service.backend}-{service.token}'
|
||||
|
||||
try:
|
||||
service.start()
|
||||
service.start(actorType, actorId)
|
||||
except Exception as exception:
|
||||
with servicesLock:
|
||||
if serviceId not in services:
|
||||
|
|
@ -171,23 +184,63 @@ def _startRecordingService(service):
|
|||
def stopRecording(backend, token, data):
|
||||
serviceId = f'{backend}-{token}'
|
||||
|
||||
if 'stop' not in data:
|
||||
raise BadRequest()
|
||||
|
||||
actorType = None
|
||||
actorId = None
|
||||
if 'actor' in data['stop'] and 'type' in data['stop']['actor'] and 'id' in data['stop']['actor']:
|
||||
actorType = data['stop']['actor']['type']
|
||||
actorId = data['stop']['actor']['id']
|
||||
|
||||
service = None
|
||||
with servicesLock:
|
||||
if serviceId not in services and serviceId in servicesStopping:
|
||||
app.logger.info(f"Trying to stop recording again: {backend} {token}")
|
||||
return {}
|
||||
|
||||
if serviceId not in services:
|
||||
app.logger.warning(f"Trying to stop unknown recording: {backend} {token}")
|
||||
return {}
|
||||
raise NotFound()
|
||||
|
||||
service = services[serviceId]
|
||||
|
||||
services.pop(serviceId)
|
||||
|
||||
servicesStopping[serviceId] = service
|
||||
|
||||
app.logger.info(f"Stop recording: {backend} {token}")
|
||||
|
||||
serviceStopThread = Thread(target=service.stop, daemon=True)
|
||||
serviceStopThread = Thread(target=_stopRecordingService, args=[service, actorType, actorId], daemon=True)
|
||||
serviceStopThread.start()
|
||||
|
||||
return {}
|
||||
|
||||
def _stopRecordingService(service, actorType, actorId):
|
||||
"""
|
||||
Helper function to stop a recording service.
|
||||
|
||||
The recording service will be removed from the list of services being
|
||||
stopped once it is fully stopped.
|
||||
|
||||
:param service: the Service to stop.
|
||||
"""
|
||||
serviceId = f'{service.backend}-{service.token}'
|
||||
|
||||
try:
|
||||
service.stop(actorType, actorId)
|
||||
except Exception as exception:
|
||||
app.logger.exception(f"Failed to stop recording: {service.backend} {service.token}")
|
||||
finally:
|
||||
with servicesLock:
|
||||
if serviceId not in servicesStopping:
|
||||
# This should never happen.
|
||||
app.logger.error(f"Recording stopped when not in the list of stopping services: {service.backend} {service.token}")
|
||||
|
||||
return
|
||||
|
||||
servicesStopping.pop(serviceId)
|
||||
|
||||
# Despite this handler it seems that in some cases the geckodriver could have
|
||||
# been killed already when it is executed, which unfortunately prevents a proper
|
||||
# cleanup of the temporary files opened by the browser.
|
||||
|
|
|
|||
|
|
@ -178,12 +178,16 @@ class Service:
|
|||
def __del__(self):
|
||||
self._stopHelpers()
|
||||
|
||||
def start(self):
|
||||
def start(self, actorType, actorId):
|
||||
"""
|
||||
Starts the recording.
|
||||
|
||||
This method blocks until the recording ends.
|
||||
|
||||
:param actorType: the actor type of the Talk participant that started
|
||||
the recording.
|
||||
:param actorId: the actor id of the Talk participant that started the
|
||||
recording.
|
||||
:raise Exception: if the recording ends unexpectedly (including if it
|
||||
could not be started).
|
||||
"""
|
||||
|
|
@ -222,6 +226,8 @@ class Service:
|
|||
self._logger.debug("Joining call")
|
||||
self._participant.joinCall(self.token)
|
||||
|
||||
BackendNotifier.started(self.backend, self.token, self.status, actorType, actorId)
|
||||
|
||||
extensionlessFileName = f'{fullDirectory}/recording-{datetime.now().strftime("%Y%m%d-%H%M%S")}'
|
||||
|
||||
recorderArgs = getRecorderArgs(self.status, self._display.new_display_var, audioSinkIndex, width, height, extensionlessFileName)
|
||||
|
|
@ -244,20 +250,31 @@ class Service:
|
|||
except Exception as exception:
|
||||
self._stopHelpers()
|
||||
|
||||
try:
|
||||
BackendNotifier.failed(self.backend, self.token)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise
|
||||
|
||||
def stop(self):
|
||||
def stop(self, actorType, actorId):
|
||||
"""
|
||||
Stops the recording and uploads it.
|
||||
|
||||
The recording is removed from the temporary directory once uploaded,
|
||||
although it is kept if the upload fails.
|
||||
|
||||
:param actorType: the actor type of the Talk participant that stopped
|
||||
the recording.
|
||||
:param actorId: the actor id of the Talk participant that stopped the
|
||||
recording.
|
||||
:raise Exception: if the file could not be uploaded.
|
||||
"""
|
||||
|
||||
self._stopHelpers()
|
||||
|
||||
BackendNotifier.stopped(self.backend, self.token, actorType, actorId)
|
||||
|
||||
if not self._fileName:
|
||||
self._logger.error(f"Recording stopping before starting, nothing to upload")
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@
|
|||
</template>
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcNoteCard v-if="isRecording"
|
||||
<NcNoteCard v-if="isStartingRecording || isRecording"
|
||||
type="warning">
|
||||
<p>{{ t('spreed', 'The call is being recorded.') }}</p>
|
||||
</NcNoteCard>
|
||||
|
|
@ -308,8 +308,14 @@ export default {
|
|||
return this.conversation.hasCall || this.conversation.hasCallOverwrittenByChat
|
||||
},
|
||||
|
||||
isStartingRecording() {
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO_STARTING
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO_STARTING
|
||||
},
|
||||
|
||||
isRecording() {
|
||||
return this.conversation.callRecording !== CALL.RECORDING.OFF
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO
|
||||
},
|
||||
|
||||
showSilentCallOption() {
|
||||
|
|
|
|||
|
|
@ -150,8 +150,14 @@ export default {
|
|||
return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation
|
||||
},
|
||||
|
||||
isStartingRecording() {
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO_STARTING
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO_STARTING
|
||||
},
|
||||
|
||||
isRecording() {
|
||||
return this.conversation.callRecording !== CALL.RECORDING.OFF
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO
|
||||
},
|
||||
|
||||
participantType() {
|
||||
|
|
@ -299,7 +305,7 @@ export default {
|
|||
const shouldShowDeviceCheckerScreen = (BrowserStorage.getItem('showDeviceChecker' + this.token) === null
|
||||
|| BrowserStorage.getItem('showDeviceChecker' + this.token) === 'true') && !this.forceJoinCall
|
||||
console.debug(shouldShowDeviceCheckerScreen)
|
||||
if ((this.isRecording && !this.forceJoinCall) || shouldShowDeviceCheckerScreen) {
|
||||
if (((this.isStartingRecording || this.isRecording) && !this.forceJoinCall) || shouldShowDeviceCheckerScreen) {
|
||||
emit('talk:device-checker:show')
|
||||
} else {
|
||||
emit('talk:device-checker:hide')
|
||||
|
|
|
|||
|
|
@ -27,19 +27,34 @@
|
|||
:triggers="[]"
|
||||
:container="container">
|
||||
<template #trigger>
|
||||
<NcButton :disabled="!isRecording || !isModerator"
|
||||
<NcButton :disabled="(!isStartingRecording && !isRecording) || !isModerator"
|
||||
:wide="true"
|
||||
:class="{ 'call-time__not-recording': !isRecording }"
|
||||
:class="{ 'call-time__not-recording': !isStartingRecording && !isRecording }"
|
||||
:title="isStartingRecording ? t('spreed', 'Starting the recording') : t('spreed', 'Recording')"
|
||||
type="tertiary"
|
||||
@click="showPopover = true">
|
||||
<template v-if="isRecording" #icon>
|
||||
<template v-if="isStartingRecording" #icon>
|
||||
<RecordCircle :size="20"
|
||||
fill-color="var(--color-loading-light)" />
|
||||
</template>
|
||||
<template v-else-if="isRecording" #icon>
|
||||
<RecordCircle :size="20"
|
||||
fill-color="#e9322d" />
|
||||
</template>
|
||||
{{ formattedTime }}
|
||||
</ncbutton>
|
||||
</template>
|
||||
<NcButton type="tertiary-no-background"
|
||||
<NcButton v-if="isStartingRecording"
|
||||
type="tertiary-no-background"
|
||||
:wide="true"
|
||||
@click="stopRecording">
|
||||
<template #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ t('spreed', 'Cancel recording start') }}
|
||||
</NcButton>
|
||||
<NcButton v-else
|
||||
type="tertiary-no-background"
|
||||
:wide="true"
|
||||
@click="stopRecording">
|
||||
<template #icon>
|
||||
|
|
@ -54,6 +69,7 @@
|
|||
|
||||
import RecordCircle from 'vue-material-design-icons/RecordCircle.vue'
|
||||
import StopIcon from 'vue-material-design-icons/Stop.vue'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import NcPopover from '@nextcloud/vue/dist/Components/NcPopover.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import { CALL } from '../../constants.js'
|
||||
|
|
@ -65,6 +81,7 @@ export default {
|
|||
components: {
|
||||
RecordCircle,
|
||||
StopIcon,
|
||||
NcLoadingIcon,
|
||||
NcPopover,
|
||||
NcButton,
|
||||
},
|
||||
|
|
@ -139,8 +156,14 @@ export default {
|
|||
return this.$store.getters.conversation(this.token) || this.$store.getters.dummyConversation
|
||||
},
|
||||
|
||||
isStartingRecording() {
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO_STARTING
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO_STARTING
|
||||
},
|
||||
|
||||
isRecording() {
|
||||
return this.conversation.callRecording !== CALL.RECORDING.OFF
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@
|
|||
</template>
|
||||
<!-- Call recording -->
|
||||
<template v-if="canModerateRecording">
|
||||
<NcActionButton v-if="!isRecording && isInCall"
|
||||
<NcActionButton v-if="!isRecording && !isStartingRecording && isInCall"
|
||||
:close-after-click="true"
|
||||
@click="startRecording">
|
||||
<template #icon>
|
||||
|
|
@ -125,6 +125,14 @@
|
|||
</template>
|
||||
{{ t('spreed', 'Start recording') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton v-else-if="isStartingRecording && isInCall"
|
||||
:close-after-click="true"
|
||||
@click="stopRecording">
|
||||
<template #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ t('spreed', 'Cancel recording start') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton v-else-if="isRecording && isInCall"
|
||||
:close-after-click="true"
|
||||
@click="stopRecording">
|
||||
|
|
@ -160,6 +168,7 @@ import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
|
|||
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
|
||||
import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
|
||||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
|
||||
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
|
|
@ -190,6 +199,7 @@ export default {
|
|||
NcActionSeparator,
|
||||
NcActionLink,
|
||||
NcActionButton,
|
||||
NcLoadingIcon,
|
||||
PromotedView,
|
||||
Cog,
|
||||
DotsHorizontal,
|
||||
|
|
@ -356,8 +366,14 @@ export default {
|
|||
return this.canFullModerate && recordingEnabled
|
||||
},
|
||||
|
||||
isStartingRecording() {
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO_STARTING
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO_STARTING
|
||||
},
|
||||
|
||||
isRecording() {
|
||||
return this.conversation.callRecording !== CALL.RECORDING.OFF
|
||||
return this.conversation.callRecording === CALL.RECORDING.VIDEO
|
||||
|| this.conversation.callRecording === CALL.RECORDING.AUDIO
|
||||
},
|
||||
|
||||
// True if current conversation is a breakout room and the brekour room has started
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ export const CALL = {
|
|||
OFF: 0,
|
||||
VIDEO: 1,
|
||||
AUDIO: 2,
|
||||
VIDEO_STARTING: 3,
|
||||
AUDIO_STARTING: 4,
|
||||
FAILED: 5,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
16
src/init.js
16
src/init.js
|
|
@ -23,6 +23,8 @@
|
|||
// The purpose of this file is to wrap the logic shared by the different talk
|
||||
// entry points
|
||||
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { CALL, PARTICIPANT } from './constants.js'
|
||||
import store from './store/index.js'
|
||||
import { EventBus } from './services/EventBus.js'
|
||||
|
||||
|
|
@ -66,4 +68,18 @@ EventBus.$on('signaling-join-room', (payload) => {
|
|||
|
||||
EventBus.$on('signaling-recording-status-changed', (token, status) => {
|
||||
store.dispatch('setConversationProperties', { token, properties: { callRecording: status } })
|
||||
|
||||
if (status !== CALL.RECORDING.FAILED) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!store.getters.isInCall(store.getters.getToken())) {
|
||||
return
|
||||
}
|
||||
|
||||
const conversation = store.getters.conversation(store.getters.getToken())
|
||||
if (conversation?.participantType === PARTICIPANT.TYPE.OWNER
|
||||
|| conversation?.participantType === PARTICIPANT.TYPE.MODERATOR) {
|
||||
showError(t('spreed', 'The recording failed. Please contact your administrator.'))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -684,18 +684,27 @@ const actions = {
|
|||
console.error(e)
|
||||
}
|
||||
|
||||
showSuccess(t('spreed', 'Call recording started.'))
|
||||
context.commit('setCallRecording', { token, callRecording })
|
||||
const startingCallRecording = callRecording === CALL.RECORDING.VIDEO ? CALL.RECORDING.VIDEO_STARTING : CALL.RECORDING.AUDIO_STARTING
|
||||
|
||||
showSuccess(t('spreed', 'Call recording is starting.'))
|
||||
context.commit('setCallRecording', { token, callRecording: startingCallRecording })
|
||||
},
|
||||
|
||||
async stopCallRecording(context, { token }) {
|
||||
const previousCallRecordingStatus = context.getters.conversation(token).callRecording
|
||||
|
||||
try {
|
||||
await stopCallRecording(token)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
showInfo(t('spreed', 'Call recording stopped. You will be notified once the recording is available.'))
|
||||
if (previousCallRecordingStatus === CALL.RECORDING.VIDEO_STARTING
|
||||
|| previousCallRecordingStatus === CALL.RECORDING.VIDEO_STARTING) {
|
||||
showInfo(t('spreed', 'Call recording stopped while starting.'))
|
||||
} else {
|
||||
showInfo(t('spreed', 'Call recording stopped. You will be notified once the recording is available.'))
|
||||
}
|
||||
context.commit('setCallRecording', { token, callRecording: CALL.RECORDING.OFF })
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ let tokensInSignaling = {}
|
|||
|
||||
/**
|
||||
* @param {string} token The token of the conversation to get the signaling settings for
|
||||
* @param {object} options The additional options for the request
|
||||
*/
|
||||
async function getSignalingSettings(token, options) {
|
||||
// If getSignalingSettings is called again while a previous one was still
|
||||
|
|
@ -79,6 +80,12 @@ async function getSignalingSettings(token, options) {
|
|||
return settings
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} token The token of the conversation to get the signaling settings for
|
||||
* @param {string} random A string of at least 32 characters
|
||||
* @param {string} checksum The SHA-256 HMAC of random with the secret of the
|
||||
* recording server
|
||||
*/
|
||||
async function signalingGetSettingsForRecording(token, random, checksum) {
|
||||
const options = {
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ if (preg_match('/\/api\/v1\/welcome/', $_SERVER['REQUEST_URI'])) {
|
|||
'data' => $data,
|
||||
];
|
||||
file_put_contents($receivedRequestsFile, json_encode($receivedRequests));
|
||||
} elseif (preg_match('/requests/', $_SERVER['REQUEST_URI'])) {
|
||||
} elseif (preg_match('/\/fake\/requests/', $_SERVER['REQUEST_URI'])) {
|
||||
if (!file_exists($receivedRequestsFile)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -74,6 +74,25 @@ if (preg_match('/\/api\/v1\/welcome/', $_SERVER['REQUEST_URI'])) {
|
|||
unlink($receivedRequestsFile);
|
||||
|
||||
echo $requests;
|
||||
} elseif (preg_match('/\/fake\/send-backend-request/', $_SERVER['REQUEST_URI'])) {
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $_SERVER['HTTP_BACKEND_URL']);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'OCS-APiRequest: true',
|
||||
'Talk-Recording-Random: ' . $_SERVER['HTTP_TALK_RECORDING_RANDOM'],
|
||||
'Talk-Recording-Checksum: ' . $_SERVER['HTTP_TALK_RECORDING_CHECKSUM'],
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('php://input'));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
http_response_code($responseCode);
|
||||
echo $result;
|
||||
} else {
|
||||
header('HTTP/1.0 404 Not Found');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
|
|||
self::$tokenToIdentifier = [];
|
||||
self::$sessionIdToUser = [
|
||||
'cli' => 'cli',
|
||||
'failed-to-get-session' => 'failed-to-get-session',
|
||||
];
|
||||
self::$userToSessionId = [];
|
||||
self::$userToAttendeeId = [];
|
||||
|
|
@ -3336,6 +3337,8 @@ class FeatureContext implements Context, SnippetAcceptingContext {
|
|||
$options['form_params'] = $fd;
|
||||
} elseif (is_array($body)) {
|
||||
$options['form_params'] = $body;
|
||||
} elseif (is_string($body)) {
|
||||
$options['body'] = $body;
|
||||
}
|
||||
|
||||
$options['headers'] = array_merge($headers, [
|
||||
|
|
|
|||
|
|
@ -22,8 +22,13 @@ use Behat\Gherkin\Node\TableNode;
|
|||
use GuzzleHttp\Client;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
// setAppConfig() method is expected to be available in the class that uses this
|
||||
// trait.
|
||||
// The following attributes and methods are expected to be available in the
|
||||
// class that uses this trait:
|
||||
// - baseUrl
|
||||
// - assertStatusCode()
|
||||
// - sendRequest()
|
||||
// - sendRequestFullUrl()
|
||||
// - setAppConfig()
|
||||
trait RecordingTrait {
|
||||
/** @var string */
|
||||
private $recordingServerPid = '';
|
||||
|
|
@ -40,7 +45,7 @@ trait RecordingTrait {
|
|||
}
|
||||
|
||||
// "the secret" is hardcoded in the fake recording server.
|
||||
$this->setAppConfig('spreed', new TableNode([['recording_servers', json_encode(['servers' => [['server' => 'http://127.0.0.1:9000']], 'secret' => 'the secret'])]]));
|
||||
$this->setAppConfig('spreed', new TableNode([['recording_servers', json_encode(['servers' => [['server' => 'http://' . $this->recordingServerAddress]], 'secret' => 'the secret'])]]));
|
||||
|
||||
$this->recordingServerPid = exec('php -S ' . $this->recordingServerAddress . ' features/bootstrap/FakeRecordingServer.php >/dev/null & echo $!');
|
||||
}
|
||||
|
|
@ -63,6 +68,88 @@ trait RecordingTrait {
|
|||
$this->recordingServerPid = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^recording server sent started request for "(audio|video)" recording in room "([^"]*)" as "([^"]*)" with (\d+)(?: \((v1)\))?$/
|
||||
*/
|
||||
public function recordingServerSentStartedRequestForRecordingInRoomAsWith(string $recordingType, string $identifier, string $user, int $statusCode, string $apiVersion = 'v1') {
|
||||
$recordingTypes = [
|
||||
'video' => 1,
|
||||
'audio' => 2,
|
||||
];
|
||||
|
||||
$data = [
|
||||
'type' => 'started',
|
||||
'started' => [
|
||||
'token' => FeatureContext::getTokenForIdentifier($identifier),
|
||||
'status' => $recordingTypes[$recordingType],
|
||||
'actor' => [
|
||||
'type' => 'users',
|
||||
'id' => $user,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->sendBackendRequestFromRecordingServer($data, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^recording server sent stopped request for recording in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
|
||||
*/
|
||||
public function recordingServerSentStoppedRequestForRecordingInRoomWith(string $identifier, int $statusCode, string $apiVersion = 'v1') {
|
||||
$this->recordingServerSentStoppedRequestForRecordingInRoomAsWith($identifier, null, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^recording server sent stopped request for recording in room "([^"]*)" as "([^"]*)" with (\d+)(?: \((v1)\))?$/
|
||||
*/
|
||||
public function recordingServerSentStoppedRequestForRecordingInRoomAsWith(string $identifier, ?string $user, int $statusCode, string $apiVersion = 'v1') {
|
||||
$data = [
|
||||
'type' => 'stopped',
|
||||
'stopped' => [
|
||||
'token' => FeatureContext::getTokenForIdentifier($identifier),
|
||||
],
|
||||
];
|
||||
|
||||
if ($user !== null) {
|
||||
$data['stopped']['actor'] = [
|
||||
'type' => 'users',
|
||||
'id' => $user,
|
||||
];
|
||||
}
|
||||
|
||||
$this->sendBackendRequestFromRecordingServer($data, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* @When /^recording server sent failed request for recording in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
|
||||
*/
|
||||
public function recordingServerSentFailedRequestForRecordingInRoomWith(string $identifier, int $statusCode, string $apiVersion = 'v1') {
|
||||
$data = [
|
||||
'type' => 'failed',
|
||||
'failed' => [
|
||||
'token' => FeatureContext::getTokenForIdentifier($identifier),
|
||||
],
|
||||
];
|
||||
|
||||
$this->sendBackendRequestFromRecordingServer($data, $statusCode, $apiVersion);
|
||||
}
|
||||
|
||||
private function sendBackendRequestFromRecordingServer(array $data, int $statusCode, string $apiVersion = 'v1') {
|
||||
$body = json_encode($data);
|
||||
|
||||
$random = md5((string) rand());
|
||||
$checksum = hash_hmac('sha256', $random . $body, "the secret");
|
||||
|
||||
$headers = [
|
||||
'Backend-Url' => $this->baseUrl . 'ocs/v2.php/apps/spreed/api/' . $apiVersion . '/recording/backend',
|
||||
'Talk-Recording-Random' => $random,
|
||||
'Talk-Recording-Checksum' => $checksum,
|
||||
];
|
||||
|
||||
$this->sendRequestFullUrl('POST', 'http://' . $this->recordingServerAddress . '/fake/send-backend-request', $body, $headers);
|
||||
$this->assertStatusCode($this->response, $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^recording server received the following requests$/
|
||||
*/
|
||||
|
|
@ -91,7 +178,7 @@ trait RecordingTrait {
|
|||
}
|
||||
|
||||
private function getRecordingServerReceivedRequests() {
|
||||
$url = 'http://' . $this->recordingServerAddress . '/requests';
|
||||
$url = 'http://' . $this->recordingServerAddress . '/fake/requests';
|
||||
$client = new Client();
|
||||
$response = $client->get($url);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ Feature: callapi/recording
|
|||
And user "participant1" joins room "room1" with 200 (v4)
|
||||
And user "participant1" joins call "room1" with 200 (v4)
|
||||
When user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
Then recording server received the following requests
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1"}} |
|
||||
And user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 3 |
|
||||
And recording server sent started request for "video" recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
|
|
@ -25,10 +29,14 @@ Feature: callapi/recording
|
|||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
When user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
Then recording server received the following requests
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop"} |
|
||||
And user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_stopped |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
|
|
@ -48,10 +56,14 @@ Feature: callapi/recording
|
|||
And user "participant1" joins room "room1" with 200 (v4)
|
||||
And user "participant1" joins call "room1" with 200 (v4)
|
||||
When user "participant1" starts "audio" recording in room "room1" with 200 (v1)
|
||||
Then recording server received the following requests
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1"}} |
|
||||
And user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 4 |
|
||||
And recording server sent started request for "audio" recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | audio_recording_started |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
|
|
@ -60,10 +72,14 @@ Feature: callapi/recording
|
|||
| type | name | callRecording |
|
||||
| 2 | room1 | 2 |
|
||||
When user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
Then recording server received the following requests
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop"} |
|
||||
And user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 2 |
|
||||
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | audio_recording_stopped |
|
||||
| room1 | users | participant1 | participant1-displayname | audio_recording_started |
|
||||
|
|
@ -73,6 +89,178 @@ Feature: callapi/recording
|
|||
| type | name | callRecording |
|
||||
| 2 | room1 | 0 |
|
||||
|
||||
Scenario: Recording failed to start
|
||||
Given recording server is started
|
||||
And the following "spreed" app config is set
|
||||
| signaling_dev | yes |
|
||||
And user "participant1" creates room "room1" (v4)
|
||||
| roomType | 2 |
|
||||
| roomName | room1 |
|
||||
And user "participant1" joins room "room1" with 200 (v4)
|
||||
And user "participant1" joins call "room1" with 200 (v4)
|
||||
And user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 3 |
|
||||
When recording server sent failed request for recording in room "room1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
| room1 | users | participant1 | participant1-displayname | conversation_created |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 5 |
|
||||
|
||||
Scenario: Video recording failed
|
||||
Given recording server is started
|
||||
And the following "spreed" app config is set
|
||||
| signaling_dev | yes |
|
||||
And user "participant1" creates room "room1" (v4)
|
||||
| roomType | 2 |
|
||||
| roomName | room1 |
|
||||
And user "participant1" joins room "room1" with 200 (v4)
|
||||
And user "participant1" joins call "room1" with 200 (v4)
|
||||
And user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 3 |
|
||||
And recording server sent started request for "video" recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
When recording server sent failed request for recording in room "room1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | guests | failed-to-get-session | | recording_failed |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
| room1 | users | participant1 | participant1-displayname | conversation_created |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 5 |
|
||||
|
||||
Scenario: Start and stop recording again after the previous one failed to start
|
||||
Given recording server is started
|
||||
And the following "spreed" app config is set
|
||||
| signaling_dev | yes |
|
||||
And user "participant1" creates room "room1" (v4)
|
||||
| roomType | 2 |
|
||||
| roomName | room1 |
|
||||
And user "participant1" joins room "room1" with 200 (v4)
|
||||
And user "participant1" joins call "room1" with 200 (v4)
|
||||
And user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 3 |
|
||||
And recording server sent failed request for recording in room "room1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 5 |
|
||||
When user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 3 |
|
||||
And recording server sent started request for "video" recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
| room1 | users | participant1 | participant1-displayname | conversation_created |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
When user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_stopped |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
| room1 | users | participant1 | participant1-displayname | conversation_created |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 0 |
|
||||
|
||||
Scenario: Start and stop recording again after the previous one failed
|
||||
Given recording server is started
|
||||
And the following "spreed" app config is set
|
||||
| signaling_dev | yes |
|
||||
And user "participant1" creates room "room1" (v4)
|
||||
| roomType | 2 |
|
||||
| roomName | room1 |
|
||||
And user "participant1" joins room "room1" with 200 (v4)
|
||||
And user "participant1" joins call "room1" with 200 (v4)
|
||||
And user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 3 |
|
||||
And recording server sent started request for "video" recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
And recording server sent failed request for recording in room "room1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 5 |
|
||||
When user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 3 |
|
||||
And recording server sent started request for "video" recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | guests | failed-to-get-session | | recording_failed |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
| room1 | users | participant1 | participant1-displayname | conversation_created |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
When user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 1 |
|
||||
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
|
||||
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)
|
||||
| room | actorType | actorId | actorDisplayName | systemMessage |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_stopped |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | guests | failed-to-get-session | | recording_failed |
|
||||
| room1 | users | participant1 | participant1-displayname | recording_started |
|
||||
| room1 | users | participant1 | participant1-displayname | call_started |
|
||||
| room1 | users | participant1 | participant1-displayname | conversation_created |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 0 |
|
||||
|
||||
Scenario: Get error when start|stop recording and already did this
|
||||
Given recording server is started
|
||||
And the following "spreed" app config is set
|
||||
|
|
@ -85,7 +273,8 @@ Feature: callapi/recording
|
|||
When user "participant1" starts "audio" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1"}} |
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And recording server sent started request for "audio" recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" starts "audio" recording in room "room1" with 400 (v1)
|
||||
Then the response error matches with "recording"
|
||||
And recording server received the following requests
|
||||
|
|
@ -95,7 +284,12 @@ Feature: callapi/recording
|
|||
When user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop"} |
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
Then recording server received the following requests
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
|
|
@ -104,7 +298,8 @@ Feature: callapi/recording
|
|||
When user "participant1" starts "video" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1"}} |
|
||||
| room1 | {"type":"start","start":{"status":1,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And recording server sent started request for "video" recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" starts "video" recording in room "room1" with 400 (v1)
|
||||
Then the response error matches with "recording"
|
||||
And recording server received the following requests
|
||||
|
|
@ -114,7 +309,12 @@ Feature: callapi/recording
|
|||
When user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop"} |
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" stops recording in room "room1" with 200 (v1)
|
||||
Then recording server received the following requests
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
|
|
@ -220,14 +420,19 @@ Feature: callapi/recording
|
|||
And user "participant1" starts "audio" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1"}} |
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And recording server sent started request for "audio" recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 2 |
|
||||
When user "participant1" ends call "room1" with 200 (v4)
|
||||
Then recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop"} |
|
||||
| room1 | {"type":"stop","stop":{"actor":{"type":"users","id":"participant1"}}} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 2 |
|
||||
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 0 |
|
||||
|
|
@ -244,14 +449,19 @@ Feature: callapi/recording
|
|||
And user "participant1" starts "audio" recording in room "room1" with 200 (v1)
|
||||
And recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1"}} |
|
||||
| room1 | {"type":"start","start":{"status":2,"owner":"participant1","actor":{"type":"users","id":"participant1"}}} |
|
||||
And recording server sent started request for "audio" recording in room "room1" as "participant1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 2 |
|
||||
When user "participant1" leaves room "room1" with 200 (v4)
|
||||
Then recording server received the following requests
|
||||
| token | data |
|
||||
| room1 | {"type":"stop"} |
|
||||
| room1 | {"type":"stop","stop":[]} |
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 2 |
|
||||
And recording server sent stopped request for recording in room "room1" with 200
|
||||
And user "participant1" is participant of the following unordered rooms (v4)
|
||||
| type | name | callRecording |
|
||||
| 2 | room1 | 0 |
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ use OCA\Talk\Chat\ChatManager;
|
|||
use OCA\Talk\Chat\SystemMessage\Listener;
|
||||
use OCA\Talk\Events\AddParticipantsEvent;
|
||||
use OCA\Talk\Events\ModifyParticipantEvent;
|
||||
use OCA\Talk\Events\ModifyRoomEvent;
|
||||
use OCA\Talk\Model\Attendee;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
|
|
@ -317,4 +318,291 @@ class ListenerTest extends TestCase {
|
|||
|
||||
$this->dispatch(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $event);
|
||||
}
|
||||
|
||||
public function callRecordingChangeProvider() {
|
||||
return [
|
||||
[
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
Room::RECORDING_NONE,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
Room::RECORDING_NONE,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO,
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
null,
|
||||
null,
|
||||
['message' => 'recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO,
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
['message' => 'recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO,
|
||||
Room::RECORDING_NONE,
|
||||
null,
|
||||
null,
|
||||
['message' => 'recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO,
|
||||
Room::RECORDING_NONE,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
['message' => 'recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
Room::RECORDING_NONE,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
Room::RECORDING_NONE,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO,
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
null,
|
||||
null,
|
||||
['message' => 'audio_recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO,
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
['message' => 'audio_recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO,
|
||||
Room::RECORDING_NONE,
|
||||
null,
|
||||
null,
|
||||
['message' => 'audio_recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO,
|
||||
Room::RECORDING_NONE,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
['message' => 'audio_recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
Attendee::ACTOR_USERS,
|
||||
'bob',
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_VIDEO,
|
||||
null,
|
||||
null,
|
||||
['message' => 'recording_stopped', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_VIDEO,
|
||||
Attendee::ACTOR_USERS,
|
||||
'bob',
|
||||
['message' => 'recording_stopped', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
Attendee::ACTOR_USERS,
|
||||
'bob',
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_AUDIO,
|
||||
null,
|
||||
null,
|
||||
['message' => 'audio_recording_stopped', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_NONE,
|
||||
Room::RECORDING_AUDIO,
|
||||
Attendee::ACTOR_USERS,
|
||||
'bob',
|
||||
['message' => 'audio_recording_stopped', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_FAILED,
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_FAILED,
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_FAILED,
|
||||
Room::RECORDING_VIDEO,
|
||||
null,
|
||||
null,
|
||||
['message' => 'recording_failed', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_FAILED,
|
||||
Room::RECORDING_AUDIO,
|
||||
null,
|
||||
null,
|
||||
['message' => 'recording_failed', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
Room::RECORDING_FAILED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO_STARTING,
|
||||
Room::RECORDING_FAILED,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO,
|
||||
Room::RECORDING_FAILED,
|
||||
null,
|
||||
null,
|
||||
['message' => 'recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_VIDEO,
|
||||
Room::RECORDING_FAILED,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
['message' => 'recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
Room::RECORDING_FAILED,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO_STARTING,
|
||||
Room::RECORDING_FAILED,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
null,
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO,
|
||||
Room::RECORDING_FAILED,
|
||||
null,
|
||||
null,
|
||||
['message' => 'audio_recording_started', 'parameters' => []],
|
||||
],
|
||||
[
|
||||
Room::RECORDING_AUDIO,
|
||||
Room::RECORDING_FAILED,
|
||||
Attendee::ACTOR_USERS,
|
||||
'alice',
|
||||
['message' => 'audio_recording_started', 'parameters' => []],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider callRecordingChangeProvider
|
||||
*
|
||||
* @param int $newStatus
|
||||
* @param int $oldStatus
|
||||
* @param string|null $actorType
|
||||
* @param string|null $actorId
|
||||
* @param array $expectedMessage
|
||||
*/
|
||||
public function testAfterCallRecordingSet(int $newStatus, int $oldStatus, ?string $actorType, ?string $actorId, ?array $expectedMessage): void {
|
||||
$this->mockLoggedInUser('logged_in_user');
|
||||
|
||||
$room = $this->createMock(Room::class);
|
||||
$room->expects($this->any())
|
||||
->method('getType')
|
||||
->willReturn(Room::TYPE_PUBLIC);
|
||||
|
||||
if ($actorType !== null && $actorId !== null) {
|
||||
$attendee = new Attendee();
|
||||
$attendee->setActorType($actorType);
|
||||
$attendee->setActorId($actorId);
|
||||
|
||||
$participant = $this->createMock(Participant::class);
|
||||
$participant->method('getAttendee')->willReturn($attendee);
|
||||
|
||||
$expectedActorType = $actorType;
|
||||
$expectedActorId = $actorId;
|
||||
} else {
|
||||
$participant = null;
|
||||
|
||||
$expectedActorType = Attendee::ACTOR_USERS;
|
||||
$expectedActorId = 'logged_in_user';
|
||||
}
|
||||
|
||||
$event = new ModifyRoomEvent($room, 'callRecording', $newStatus, $oldStatus, $participant);
|
||||
|
||||
if ($expectedMessage !== null) {
|
||||
$this->chatManager->expects($this->once())
|
||||
->method('addSystemMessage')
|
||||
->with(
|
||||
$room,
|
||||
$expectedActorType,
|
||||
$expectedActorId,
|
||||
json_encode($expectedMessage),
|
||||
$this->dummyTime,
|
||||
false,
|
||||
SELF::DUMMY_REFERENCE_ID,
|
||||
null,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
$this->chatManager->expects($this->never())
|
||||
->method('addSystemMessage');
|
||||
}
|
||||
|
||||
$this->dispatch(Room::EVENT_AFTER_SET_CALL_RECORDING, $event);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ use OCA\Talk\Model\SessionMapper;
|
|||
use OCA\Talk\Recording\BackendNotifier;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\RoomService;
|
||||
use OCA\Talk\TalkSession;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
|
|
@ -39,6 +40,7 @@ use OCP\Http\Client\IClientService;
|
|||
use OCP\IGroupManager;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Security\IHasher;
|
||||
use OCP\Security\ISecureRandom;
|
||||
|
|
@ -75,6 +77,8 @@ class BackendNotifierTest extends TestCase {
|
|||
private $urlGenerator;
|
||||
private ?\OCA\Talk\Tests\php\Recording\CustomBackendNotifier $backendNotifier = null;
|
||||
|
||||
/** @var ParticipantService|MockObject */
|
||||
private $participantService;
|
||||
private ?Manager $manager = null;
|
||||
|
||||
private ?string $recordingSecret = null;
|
||||
|
|
@ -107,6 +111,8 @@ class BackendNotifierTest extends TestCase {
|
|||
|
||||
$this->recreateBackendNotifier();
|
||||
|
||||
$this->participantService = \OC::$server->get(ParticipantService::class);
|
||||
|
||||
$dbConnection = \OC::$server->getDatabaseConnection();
|
||||
$this->manager = new Manager(
|
||||
$dbConnection,
|
||||
|
|
@ -115,7 +121,7 @@ class BackendNotifierTest extends TestCase {
|
|||
\OC::$server->get(IAppManager::class),
|
||||
\OC::$server->get(AttendeeMapper::class),
|
||||
\OC::$server->get(SessionMapper::class),
|
||||
$this->createMock(ParticipantService::class),
|
||||
$this->participantService,
|
||||
$this->secureRandom,
|
||||
$this->createMock(IUserManager::class),
|
||||
$groupManager,
|
||||
|
|
@ -182,26 +188,78 @@ class BackendNotifierTest extends TestCase {
|
|||
}
|
||||
|
||||
public function testStart() {
|
||||
$room = $this->manager->createRoom(Room::TYPE_PUBLIC);
|
||||
$userId = 'testUser';
|
||||
|
||||
$this->backendNotifier->start($room, Room::RECORDING_VIDEO, 'participant1');
|
||||
/** @var IUser|MockObject $testUser */
|
||||
$testUser = $this->createMock(IUser::class);
|
||||
$testUser->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn($userId);
|
||||
|
||||
$roomService = $this->createMock(RoomService::class);
|
||||
$roomService->method('verifyPassword')
|
||||
->willReturn(['result' => true, 'url' => '']);
|
||||
|
||||
$room = $this->manager->createRoom(Room::TYPE_PUBLIC);
|
||||
$this->participantService->addUsers($room, [[
|
||||
'actorType' => 'users',
|
||||
'actorId' => $userId,
|
||||
]]);
|
||||
$participant = $this->participantService->joinRoom($roomService, $room, $testUser, '');
|
||||
|
||||
$this->backendNotifier->start($room, Room::RECORDING_VIDEO, 'participant1', $participant);
|
||||
|
||||
$this->assertMessageWasSent($room, [
|
||||
'type' => 'start',
|
||||
'start' => [
|
||||
'status' => Room::RECORDING_VIDEO,
|
||||
'owner' => 'participant1',
|
||||
'actor' => [
|
||||
'type' => 'users',
|
||||
'id' => $userId,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testStop() {
|
||||
$userId = 'testUser';
|
||||
|
||||
/** @var IUser|MockObject $testUser */
|
||||
$testUser = $this->createMock(IUser::class);
|
||||
$testUser->expects($this->any())
|
||||
->method('getUID')
|
||||
->willReturn($userId);
|
||||
|
||||
$roomService = $this->createMock(RoomService::class);
|
||||
$roomService->method('verifyPassword')
|
||||
->willReturn(['result' => true, 'url' => '']);
|
||||
|
||||
$room = $this->manager->createRoom(Room::TYPE_PUBLIC);
|
||||
$this->participantService->addUsers($room, [[
|
||||
'actorType' => 'users',
|
||||
'actorId' => $userId,
|
||||
]]);
|
||||
$participant = $this->participantService->joinRoom($roomService, $room, $testUser, '');
|
||||
|
||||
$this->backendNotifier->stop($room, $participant);
|
||||
|
||||
$this->assertMessageWasSent($room, [
|
||||
'type' => 'stop',
|
||||
'stop' => [
|
||||
'actor' => [
|
||||
'type' => 'users',
|
||||
'id' => $userId,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->backendNotifier->stop($room);
|
||||
|
||||
$this->assertMessageWasSent($room, [
|
||||
'type' => 'stop',
|
||||
'stop' => [
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
tests/stubs/GuzzleHttp_Exception_ClientException.php
Normal file
8
tests/stubs/GuzzleHttp_Exception_ClientException.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace GuzzleHttp\Exception;
|
||||
|
||||
class ClientException extends \RuntimeException {
|
||||
public function getResponse() {
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue