UPSTREAM BASELINE: nextcloud/spreed v22.0.12 (без изменений)
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Type checking / changes (push) Has been cancelled
Type checking / test (push) Has been cancelled
Type checking / typescript-summary (push) Has been cancelled
Node tests / changes (push) Has been cancelled
Node tests / test (push) Has been cancelled
Node tests / test-summary (push) Has been cancelled
Источник: https://github.com/nextcloud/spreed/archive/refs/tags/v22.0.12.tar.gz С этого коммита ветка официального Nextcloud Talk отрезана (решение владельца 2026-07-06). Все дальнейшие изменения — только наши; версии релизов: 22.0.12-f7.N.
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Signaling;
|
||||
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use OC\Http\Client\Response;
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Model\Attendee;
|
||||
use OCA\Talk\Model\Session;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class BackendNotifier {
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private IAppConfig $appConfig,
|
||||
private LoggerInterface $logger,
|
||||
private IClientService $clientService,
|
||||
private ISecureRandom $secureRandom,
|
||||
private Manager $signalingManager,
|
||||
private ParticipantService $participantService,
|
||||
private IURLGenerator $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform actual network request to the signaling backend.
|
||||
* This can be overridden in tests.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $params
|
||||
* @param int $retries
|
||||
* @return ?IResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function doRequest(string $url, array $params, int $retries = 3): ?IResponse {
|
||||
if (defined('PHPUNIT_RUN')) {
|
||||
// Don't perform network requests when running tests.
|
||||
return null;
|
||||
}
|
||||
|
||||
$client = $this->clientService->newClient();
|
||||
try {
|
||||
$response = $client->post($url, $params);
|
||||
|
||||
if (!$this->signalingManager->isCompatibleSignalingServer($response)) {
|
||||
throw new \RuntimeException('Signaling server needs to be updated to be compatible with this version of Talk');
|
||||
}
|
||||
|
||||
return $response;
|
||||
} catch (ConnectException $e) {
|
||||
if ($retries > 1) {
|
||||
$this->logger->error('Failed to send message to signaling server, ' . $retries . ' retries left!', ['exception' => $e]);
|
||||
return $this->doRequest($url, $params, $retries - 1);
|
||||
}
|
||||
|
||||
$this->logger->error('Failed to send message to signaling server, giving up!', ['exception' => $e]);
|
||||
throw $e;
|
||||
} catch (ServerException $e) {
|
||||
if ($retries > 1) {
|
||||
$this->logger->error('Failed to send message to signaling server, ' . $retries . ' retries left!', ['exception' => $e]);
|
||||
return $this->doRequest($url, $params, $retries - 1);
|
||||
}
|
||||
|
||||
$this->logger->error('Failed to send message to signaling server, giving up!', ['exception' => $e]);
|
||||
if ($e->hasResponse()) {
|
||||
return new Response($e->getResponse());
|
||||
}
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to send message to signaling server', ['exception' => $e]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a request to the signaling backend.
|
||||
*
|
||||
* @param Room $room
|
||||
* @param array $data
|
||||
* @return ?IResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function backendRequest(Room $room, array $data): ?IResponse {
|
||||
if ($this->config->getSignalingMode() === Config::SIGNALING_INTERNAL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// FIXME some need to go to all HPBs, but that doesn't scale, so bad luck for now :(
|
||||
$signaling = $this->signalingManager->getSignalingServerForConversation($room);
|
||||
$signaling['server'] = rtrim($signaling['server'], '/');
|
||||
|
||||
$url = '/api/v1/room/' . $room->getToken();
|
||||
$url = $signaling['server'] . $url;
|
||||
|
||||
if (str_starts_with($url, 'ws://')) {
|
||||
$url = 'http://' . substr($url, 5);
|
||||
} elseif (str_starts_with($url, 'wss://')) {
|
||||
$url = 'https://' . substr($url, 6);
|
||||
}
|
||||
|
||||
$body = json_encode($data);
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
|
||||
$random = $this->secureRandom->generate(64);
|
||||
$hash = hash_hmac('sha256', $random . $body, $this->config->getSignalingSecret());
|
||||
$headers['Spreed-Signaling-Random'] = $random;
|
||||
$headers['Spreed-Signaling-Checksum'] = $hash;
|
||||
$headers['Spreed-Signaling-Backend'] = $this->urlGenerator->getAbsoluteURL('');
|
||||
|
||||
$params = [
|
||||
'headers' => $headers,
|
||||
'body' => $body,
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => true,
|
||||
],
|
||||
];
|
||||
if (empty($signaling['verify'])) {
|
||||
$params['verify'] = false;
|
||||
}
|
||||
return $this->doRequest($url, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* The given users are now invited to a room.
|
||||
*
|
||||
* @param Room $room
|
||||
* @param Attendee[] $attendees
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function roomInvited(Room $room, array $attendees): void {
|
||||
$userIds = [];
|
||||
foreach ($attendees as $attendee) {
|
||||
if ($attendee->getActorType() === Attendee::ACTOR_USERS) {
|
||||
$userIds[] = $attendee->getActorId();
|
||||
}
|
||||
}
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'invite',
|
||||
'invite' => [
|
||||
'userids' => $userIds,
|
||||
// TODO(fancycode): We should try to get rid of 'alluserids' and
|
||||
// find a better way to notify existing users to update the room.
|
||||
'alluserids' => $this->participantService->getParticipantUserIdsAndFederatedUserCloudIds($room),
|
||||
'properties' => $room->getPropertiesForSignaling('', false),
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Now invited to {token}: {users} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'users' => print_r($userIds, true),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The given users are no longer invited to a room.
|
||||
*
|
||||
* @param Room $room
|
||||
* @param Attendee[] $attendees
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function roomsDisinvited(Room $room, array $attendees): void {
|
||||
$allUserIds = $this->participantService->getParticipantUserIdsAndFederatedUserCloudIds($room);
|
||||
sort($allUserIds);
|
||||
$userIds = [];
|
||||
foreach ($attendees as $attendee) {
|
||||
if ($attendee->getActorType() === Attendee::ACTOR_USERS) {
|
||||
$userIds[] = $attendee->getActorId();
|
||||
}
|
||||
}
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'disinvite',
|
||||
'disinvite' => [
|
||||
'userids' => $userIds,
|
||||
// TODO(fancycode): We should try to get rid of 'alluserids' and
|
||||
// find a better way to notify existing users to update the room.
|
||||
'alluserids' => $allUserIds,
|
||||
'properties' => $room->getPropertiesForSignaling('', false),
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('No longer invited to {token}: {users} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'users' => print_r($userIds, true),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The given sessions have been removed from a room.
|
||||
*
|
||||
* @param Room $room
|
||||
* @param string[] $sessionIds
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function roomSessionsRemoved(Room $room, array $sessionIds): void {
|
||||
$allUserIds = $this->participantService->getParticipantUserIdsAndFederatedUserCloudIds($room);
|
||||
sort($allUserIds);
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'disinvite',
|
||||
'disinvite' => [
|
||||
'sessionids' => $sessionIds,
|
||||
// TODO(fancycode): We should try to get rid of 'alluserids' and
|
||||
// find a better way to notify existing users to update the room.
|
||||
'alluserids' => $allUserIds,
|
||||
'properties' => $room->getPropertiesForSignaling('', false),
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Removed from {token}: {users} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'users' => print_r($sessionIds, true),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The given room has been modified.
|
||||
*
|
||||
* @param Room $room
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function roomModified(Room $room): void {
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'update',
|
||||
'update' => [
|
||||
// Message not sent for federated users, as they will receive
|
||||
// the message from their federated Nextcloud server once the
|
||||
// property change is propagated.
|
||||
'userids' => $this->participantService->getParticipantUserIds($room),
|
||||
'properties' => $room->getPropertiesForSignaling(''),
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Room modified: {token} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The given room has been deleted.
|
||||
*
|
||||
* @param Room $room
|
||||
* @param string[] $userIds
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function roomDeleted(Room $room, array $userIds): void {
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'delete',
|
||||
'delete' => [
|
||||
'userids' => $userIds,
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Room deleted: {token} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The given participants should switch to the given room.
|
||||
*
|
||||
* @param Room $room
|
||||
* @param string $switchToRoomToken
|
||||
* @param string[] $sessionIds
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function switchToRoom(Room $room, string $switchToRoomToken, array $sessionIds): void {
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'switchto',
|
||||
'switchto' => [
|
||||
'roomid' => $switchToRoomToken,
|
||||
'sessions' => $sessionIds,
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Switch to room: {token} {roomid} {sessions} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'roomid' => $switchToRoomToken,
|
||||
'sessions' => print_r($sessionIds, true),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The participant list of the given room has been modified.
|
||||
*
|
||||
* @param Room $room
|
||||
* @param string[] $sessionIds
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function participantsModified(Room $room, array $sessionIds): void {
|
||||
$changed = [];
|
||||
$users = [];
|
||||
$participants = $this->participantService->getSessionsAndParticipantsForRoom($room);
|
||||
foreach ($participants as $participant) {
|
||||
$attendee = $participant->getAttendee();
|
||||
if ($attendee->getActorType() !== Attendee::ACTOR_USERS
|
||||
&& $attendee->getActorType() !== Attendee::ACTOR_GUESTS
|
||||
&& $attendee->getActorType() !== Attendee::ACTOR_EMAILS
|
||||
&& $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'inCall' => Participant::FLAG_DISCONNECTED,
|
||||
'lastPing' => 0,
|
||||
'sessionId' => '0',
|
||||
'participantType' => $attendee->getParticipantType(),
|
||||
'participantPermissions' => Attendee::PERMISSIONS_CUSTOM,
|
||||
'displayName' => $attendee->getDisplayName(),
|
||||
'actorType' => $attendee->getActorType(),
|
||||
'actorId' => $attendee->getActorId(),
|
||||
];
|
||||
if ($attendee->getActorType() === Attendee::ACTOR_USERS) {
|
||||
$data['userId'] = $attendee->getActorId();
|
||||
}
|
||||
|
||||
$session = $participant->getSession();
|
||||
if ($session instanceof Session) {
|
||||
$data['inCall'] = $session->getInCall();
|
||||
$data['lastPing'] = $session->getLastPing();
|
||||
$data['sessionId'] = $session->getSessionId();
|
||||
$data['participantPermissions'] = $participant->getPermissions();
|
||||
$users[] = $data;
|
||||
|
||||
if (\in_array($session->getSessionId(), $sessionIds, true)) {
|
||||
$data['permissions'] = [];
|
||||
if ($participant->getPermissions() & Attendee::PERMISSIONS_PUBLISH_AUDIO) {
|
||||
$data['permissions'][] = 'publish-audio';
|
||||
}
|
||||
if ($participant->getPermissions() & Attendee::PERMISSIONS_PUBLISH_VIDEO) {
|
||||
$data['permissions'][] = 'publish-video';
|
||||
}
|
||||
if ($participant->getPermissions() & Attendee::PERMISSIONS_PUBLISH_SCREEN) {
|
||||
$data['permissions'][] = 'publish-screen';
|
||||
}
|
||||
if ($participant->hasModeratorPermissions(false)) {
|
||||
$data['permissions'][] = 'control';
|
||||
}
|
||||
$changed[] = $data;
|
||||
}
|
||||
} else {
|
||||
$users[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'participants',
|
||||
'participants' => [
|
||||
'changed' => $changed,
|
||||
'users' => $users
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Room participants modified: {token} {users} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'users' => print_r($sessionIds, true),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The "in-call" status of the given session ids has changed..
|
||||
*
|
||||
* @param Room $room
|
||||
* @param int $flags
|
||||
* @param string[] $sessionIds
|
||||
* @param bool $changeAll
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function roomInCallChanged(Room $room, int $flags, array $sessionIds, bool $changeAll = false): void {
|
||||
if ($changeAll) {
|
||||
$data = [
|
||||
'incall' => $flags,
|
||||
'all' => true
|
||||
];
|
||||
} else {
|
||||
$changed = [];
|
||||
$users = [];
|
||||
|
||||
$participants = $this->participantService->getParticipantsForAllSessions($room);
|
||||
foreach ($participants as $participant) {
|
||||
$session = $participant->getSession();
|
||||
if (!$session instanceof Session) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attendee = $participant->getAttendee();
|
||||
if ($attendee->getActorType() !== Attendee::ACTOR_USERS
|
||||
&& $attendee->getActorType() !== Attendee::ACTOR_GUESTS
|
||||
&& $attendee->getActorType() !== Attendee::ACTOR_EMAILS
|
||||
&& $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'inCall' => $session->getInCall(),
|
||||
'lastPing' => $session->getLastPing(),
|
||||
'sessionId' => $session->getSessionId(),
|
||||
'nextcloudSessionId' => $session->getSessionId(),
|
||||
'participantType' => $attendee->getParticipantType(),
|
||||
'participantPermissions' => $participant->getPermissions(),
|
||||
'actorType' => $attendee->getActorType(),
|
||||
'actorId' => $attendee->getActorId(),
|
||||
];
|
||||
if ($attendee->getActorType() === Attendee::ACTOR_USERS) {
|
||||
$data['userId'] = $attendee->getActorId();
|
||||
}
|
||||
|
||||
if ($session->getInCall() !== Participant::FLAG_DISCONNECTED) {
|
||||
$users[] = $data;
|
||||
}
|
||||
|
||||
if (\in_array($session->getSessionId(), $sessionIds, true)) {
|
||||
$changed[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'incall' => $flags,
|
||||
'changed' => $changed,
|
||||
'users' => $users,
|
||||
];
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'incall',
|
||||
'incall' => $data,
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Room in-call status changed: {token} {flags} {users} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'flags' => $flags,
|
||||
'users' => $changeAll ? 'all' : print_r($sessionIds, true),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send dial-out requests to the HPB
|
||||
*
|
||||
* @param string|bool $callerNumber Send the call anonymous when false, default number when true otherwise the string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function dialOutToAttendee(Room $room, Attendee $attendee, string|bool $callerNumber): ?string {
|
||||
$start = microtime(true);
|
||||
$dialoutData = [
|
||||
'type' => 'dialout',
|
||||
'dialout' => [
|
||||
'number' => $attendee->getPhoneNumber(),
|
||||
'options' => [
|
||||
'attendeeId' => $attendee->getId(),
|
||||
'actorType' => $attendee->getActorType(),
|
||||
'actorId' => $attendee->getActorId(),
|
||||
]
|
||||
],
|
||||
];
|
||||
if ($callerNumber === false) {
|
||||
$dialoutData['dialout']['options']['anonymous'] = true;
|
||||
} elseif (is_string($callerNumber)) {
|
||||
$dialoutData['dialout']['options']['caller'] = $callerNumber;
|
||||
}
|
||||
$response = $this->backendRequest($room, $dialoutData);
|
||||
|
||||
if ($response === null) {
|
||||
$this->logger->debug('Room dial out response was NULL');
|
||||
return null;
|
||||
}
|
||||
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Room dial out: {token} {number} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'number' => $attendee->getPhoneNumber(),
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
|
||||
return (string)$response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to all sessions currently joined in a room. The message
|
||||
* will be received by "processRoomMessageEvent" in "signaling.js".
|
||||
*
|
||||
* @param Room $room
|
||||
* @param array $message
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function sendRoomMessage(Room $room, array $message): void {
|
||||
$start = microtime(true);
|
||||
$this->backendRequest($room, [
|
||||
'type' => 'message',
|
||||
'message' => [
|
||||
'data' => $message,
|
||||
],
|
||||
]);
|
||||
$duration = microtime(true) - $start;
|
||||
$this->logger->debug('Send room message: {token} {message} ({duration})', [
|
||||
'token' => $room->getToken(),
|
||||
'message' => $message,
|
||||
'duration' => sprintf('%.2f', $duration),
|
||||
'app' => 'spreed-hpb',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Signaling;
|
||||
|
||||
use OCA\Talk\AppInfo\Application;
|
||||
use OCA\Talk\Chat\ChatManager;
|
||||
use OCA\Talk\Chat\MessageParser;
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Events\AMessageSentEvent;
|
||||
use OCA\Talk\Events\AParticipantModifiedEvent;
|
||||
use OCA\Talk\Events\AReactionEvent;
|
||||
use OCA\Talk\Events\ARoomEvent;
|
||||
use OCA\Talk\Events\ARoomModifiedEvent;
|
||||
use OCA\Talk\Events\ASystemMessageSentEvent;
|
||||
use OCA\Talk\Events\AttendeeRemovedEvent;
|
||||
use OCA\Talk\Events\AttendeesAddedEvent;
|
||||
use OCA\Talk\Events\AttendeesRemovedEvent;
|
||||
use OCA\Talk\Events\BeforeAttendeeRemovedEvent;
|
||||
use OCA\Talk\Events\BeforeRoomDeletedEvent;
|
||||
use OCA\Talk\Events\BeforeRoomSyncedEvent;
|
||||
use OCA\Talk\Events\BeforeSessionLeftRoomEvent;
|
||||
use OCA\Talk\Events\CallEndedForEveryoneEvent;
|
||||
use OCA\Talk\Events\ChatMessageSentEvent;
|
||||
use OCA\Talk\Events\GuestJoinedRoomEvent;
|
||||
use OCA\Talk\Events\GuestsCleanedUpEvent;
|
||||
use OCA\Talk\Events\LobbyModifiedEvent;
|
||||
use OCA\Talk\Events\ParticipantModifiedEvent;
|
||||
use OCA\Talk\Events\ReactionAddedEvent;
|
||||
use OCA\Talk\Events\ReactionRemovedEvent;
|
||||
use OCA\Talk\Events\RoomExtendedEvent;
|
||||
use OCA\Talk\Events\RoomModifiedEvent;
|
||||
use OCA\Talk\Events\RoomSyncedEvent;
|
||||
use OCA\Talk\Events\SessionLeftRoomEvent;
|
||||
use OCA\Talk\Events\SystemMessageSentEvent;
|
||||
use OCA\Talk\Events\SystemMessagesMultipleSentEvent;
|
||||
use OCA\Talk\Events\UserJoinedRoomEvent;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Model\BreakoutRoom;
|
||||
use OCA\Talk\Model\Session;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\SessionService;
|
||||
use OCA\Talk\Service\ThreadService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCP\Server;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener<Event>
|
||||
*/
|
||||
class Listener implements IEventListener {
|
||||
public const EXTERNAL_SIGNALING_PROPERTIES = [
|
||||
ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_MODE,
|
||||
ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_STATUS,
|
||||
ARoomModifiedEvent::PROPERTY_CALL_RECORDING,
|
||||
ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS,
|
||||
ARoomModifiedEvent::PROPERTY_DESCRIPTION,
|
||||
ARoomModifiedEvent::PROPERTY_LISTABLE,
|
||||
ARoomModifiedEvent::PROPERTY_LOBBY,
|
||||
ARoomModifiedEvent::PROPERTY_NAME,
|
||||
ARoomModifiedEvent::PROPERTY_PASSWORD,
|
||||
ARoomModifiedEvent::PROPERTY_READ_ONLY,
|
||||
ARoomModifiedEvent::PROPERTY_SIP_ENABLED,
|
||||
ARoomModifiedEvent::PROPERTY_TYPE,
|
||||
];
|
||||
|
||||
protected bool $pauseRoomModifiedListener = false;
|
||||
|
||||
public function __construct(
|
||||
protected Config $talkConfig,
|
||||
protected Messages $internalSignaling,
|
||||
protected BackendNotifier $externalSignaling,
|
||||
protected Manager $manager,
|
||||
protected ParticipantService $participantService,
|
||||
protected SessionService $sessionService,
|
||||
protected ITimeFactory $timeFactory,
|
||||
protected MessageParser $messageParser,
|
||||
protected ThreadService $threadService,
|
||||
protected IFactory $l10nFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function handle(Event $event): void {
|
||||
if ($this->talkConfig->getSignalingMode() === Config::SIGNALING_INTERNAL) {
|
||||
$this->handleInternalSignaling($event);
|
||||
} else {
|
||||
$this->handleExternalSignaling($event);
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleInternalSignaling(Event $event): void {
|
||||
match (get_class($event)) {
|
||||
BeforeSessionLeftRoomEvent::class,
|
||||
BeforeAttendeeRemovedEvent::class,
|
||||
GuestJoinedRoomEvent::class,
|
||||
BeforeRoomDeletedEvent::class,
|
||||
UserJoinedRoomEvent::class => $this->refreshParticipantList($event->getRoom()),
|
||||
ParticipantModifiedEvent::class => $this->refreshParticipantListParticipantModified($event), // in_call, name, permissions
|
||||
RoomModifiedEvent::class => $this->refreshParticipantListRoomModified($event), // *_permissions
|
||||
default => null, // Ignoring events subscribed by the external signaling
|
||||
};
|
||||
}
|
||||
|
||||
protected function refreshParticipantList(Room $room): void {
|
||||
$this->internalSignaling->addMessageForAllParticipants($room, 'refresh-participant-list');
|
||||
}
|
||||
|
||||
protected function refreshParticipantListParticipantModified(ParticipantModifiedEvent $event): void {
|
||||
if (!in_array($event->getProperty(), [
|
||||
AParticipantModifiedEvent::PROPERTY_IN_CALL,
|
||||
AParticipantModifiedEvent::PROPERTY_NAME,
|
||||
AParticipantModifiedEvent::PROPERTY_PERMISSIONS,
|
||||
], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->refreshParticipantList($event->getRoom());
|
||||
}
|
||||
|
||||
protected function refreshParticipantListRoomModified(RoomModifiedEvent $event): void {
|
||||
if (!in_array($event->getProperty(), [
|
||||
ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS,
|
||||
], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->refreshParticipantList($event->getRoom());
|
||||
}
|
||||
|
||||
protected function handleExternalSignaling(Event $event): void {
|
||||
match (get_class($event)) {
|
||||
RoomModifiedEvent::class,
|
||||
LobbyModifiedEvent::class => $this->notifyRoomModified($event),
|
||||
RoomExtendedEvent::class => $this->notifyRoomExtended($event),
|
||||
BeforeRoomSyncedEvent::class => $this->pauseRoomModifiedListener(),
|
||||
RoomSyncedEvent::class => $this->notifyRoomSynced($event),
|
||||
BeforeRoomDeletedEvent::class => $this->notifyBeforeRoomDeleted($event),
|
||||
CallEndedForEveryoneEvent::class => $this->notifyCallEndedForEveryone($event),
|
||||
GuestsCleanedUpEvent::class => $this->notifyGuestsCleanedUp($event),
|
||||
AttendeesAddedEvent::class => $this->notifyAttendeesAdded($event),
|
||||
AttendeeRemovedEvent::class => $this->notifyAttendeeRemoved($event),
|
||||
AttendeesRemovedEvent::class => $this->notifyAttendeesRemoved($event),
|
||||
ParticipantModifiedEvent::class => $this->notifyParticipantModified($event),
|
||||
SessionLeftRoomEvent::class => $this->notifySessionLeftRoom($event),
|
||||
ChatMessageSentEvent::class,
|
||||
SystemMessageSentEvent::class,
|
||||
SystemMessagesMultipleSentEvent::class => $this->notifyMessageSent($event),
|
||||
ReactionAddedEvent::class,
|
||||
ReactionRemovedEvent::class => $this->notifyReactionSent($event),
|
||||
default => null, // Ignoring events subscribed by the internal signaling
|
||||
};
|
||||
}
|
||||
|
||||
protected function pauseRoomModifiedListener(): void {
|
||||
$this->pauseRoomModifiedListener = true;
|
||||
}
|
||||
|
||||
protected function notifyRoomModified(ARoomModifiedEvent $event): void {
|
||||
if (!in_array($event->getProperty(), self::EXTERNAL_SIGNALING_PROPERTIES, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event->getProperty() === ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS) {
|
||||
$this->notifyRoomPermissionsModified($event);
|
||||
// The room permission itself does not need a signaling message anymore
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event->getProperty() === ARoomModifiedEvent::PROPERTY_CALL_RECORDING) {
|
||||
$this->notifyRoomRecordingModified($event);
|
||||
}
|
||||
if ($event->getProperty() === ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_STATUS) {
|
||||
$this->notifyBreakoutRoomStatusModified($event);
|
||||
}
|
||||
$this->externalSignaling->roomModified($event->getRoom());
|
||||
}
|
||||
|
||||
protected function notifyRoomSynced(RoomSyncedEvent $event): void {
|
||||
$this->pauseRoomModifiedListener = false;
|
||||
if (empty(array_intersect($event->getProperties(), self::EXTERNAL_SIGNALING_PROPERTIES))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array(ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS, $event->getProperties(), true)) {
|
||||
$this->notifyRoomPermissionsModified($event);
|
||||
}
|
||||
|
||||
if (in_array(ARoomModifiedEvent::PROPERTY_CALL_RECORDING, $event->getProperties(), true)) {
|
||||
$this->notifyRoomRecordingModified($event);
|
||||
}
|
||||
|
||||
if (in_array(ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_STATUS, $event->getProperties(), true)) {
|
||||
$this->notifyBreakoutRoomStatusModified($event);
|
||||
}
|
||||
|
||||
$this->externalSignaling->roomModified($event->getRoom());
|
||||
}
|
||||
|
||||
protected function notifyRoomRecordingModified(ARoomEvent $event): void {
|
||||
$room = $event->getRoom();
|
||||
$message = [
|
||||
'type' => 'recording',
|
||||
'recording' => [
|
||||
'status' => $room->getCallRecording(),
|
||||
],
|
||||
];
|
||||
|
||||
$this->externalSignaling->sendRoomMessage($room, $message);
|
||||
}
|
||||
|
||||
protected function notifyCallEndedForEveryone(CallEndedForEveryoneEvent $event): void {
|
||||
$sessionIds = $event->getSessionIds();
|
||||
|
||||
if (empty($sessionIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->externalSignaling->roomInCallChanged(
|
||||
$event->getRoom(),
|
||||
$event->getCallFlag(),
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
protected function notifyBeforeRoomDeleted(BeforeRoomDeletedEvent $event): void {
|
||||
$room = $event->getRoom();
|
||||
$this->externalSignaling->roomDeleted($room, $this->participantService->getParticipantUserIds($room));
|
||||
}
|
||||
|
||||
protected function notifyGuestsCleanedUp(GuestsCleanedUpEvent $event): void {
|
||||
// TODO: The list of removed session ids should be passed through the event
|
||||
// so the signaling server can optimize forwarding the message.
|
||||
$sessionIds = [];
|
||||
$this->externalSignaling->participantsModified($event->getRoom(), $sessionIds);
|
||||
}
|
||||
|
||||
protected function notifyParticipantModified(AParticipantModifiedEvent $event): void {
|
||||
if ($event->getProperty() === AParticipantModifiedEvent::PROPERTY_TYPE) {
|
||||
// TODO remove handler with "roomModified" in favour of handler with
|
||||
// "participantsModified" once the clients no longer expect a
|
||||
// "roomModified" message for participant type changes.
|
||||
$this->externalSignaling->roomModified($event->getRoom());
|
||||
}
|
||||
|
||||
if ($event->getProperty() === AParticipantModifiedEvent::PROPERTY_NAME) {
|
||||
$this->notifyParticipantNameModified($event);
|
||||
}
|
||||
|
||||
if ($event->getProperty() === AParticipantModifiedEvent::PROPERTY_IN_CALL) {
|
||||
$this->notifyParticipantInCallModified($event);
|
||||
}
|
||||
|
||||
if ($event->getProperty() === AParticipantModifiedEvent::PROPERTY_TYPE
|
||||
|| $event->getProperty() === AParticipantModifiedEvent::PROPERTY_PERMISSIONS) {
|
||||
$this->notifyParticipantTypeOrPermissionsModified($event);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyParticipantNameModified(AParticipantModifiedEvent $event): void {
|
||||
$sessionIds = [];
|
||||
$sessions = $this->sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee());
|
||||
foreach ($sessions as $session) {
|
||||
$sessionIds[] = $session->getSessionId();
|
||||
}
|
||||
|
||||
if (!empty($sessionIds)) {
|
||||
$this->externalSignaling->participantsModified($event->getRoom(), $sessionIds);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyParticipantTypeOrPermissionsModified(AParticipantModifiedEvent $event): void {
|
||||
$sessionIds = [];
|
||||
|
||||
// If the participant is not active in the room the "participants"
|
||||
// request will be sent anyway, although with an empty "changed"
|
||||
// property.
|
||||
$sessions = $this->sessionService->getAllSessionsForAttendee($event->getParticipant()->getAttendee());
|
||||
foreach ($sessions as $session) {
|
||||
$sessionIds[] = $session->getSessionId();
|
||||
}
|
||||
|
||||
$this->externalSignaling->participantsModified($event->getRoom(), $sessionIds);
|
||||
}
|
||||
|
||||
protected function notifyRoomPermissionsModified(ARoomEvent $event): void {
|
||||
$sessionIds = [];
|
||||
|
||||
// Setting the room permissions resets the permissions of all
|
||||
// participants, even those with custom attendee permissions.
|
||||
|
||||
// FIXME This approach does not scale, as the update message for all
|
||||
// the sessions in a conversation can exceed the allowed size of the
|
||||
// request in conversations with a large number of participants.
|
||||
// However, note that a single message with the general permissions
|
||||
// to be set on all participants can not be sent either, as the
|
||||
// general permissions could be overriden by custom attendee
|
||||
// permissions in specific participants.
|
||||
$participants = $this->participantService->getSessionsAndParticipantsForRoom($event->getRoom());
|
||||
foreach ($participants as $participant) {
|
||||
$session = $participant->getSession();
|
||||
if ($session) {
|
||||
$sessionIds[] = $session->getSessionId();
|
||||
}
|
||||
}
|
||||
|
||||
$this->externalSignaling->participantsModified($event->getRoom(), $sessionIds);
|
||||
}
|
||||
|
||||
protected function notifyAttendeesAdded(AttendeesAddedEvent $event): void {
|
||||
$this->externalSignaling->roomInvited($event->getRoom(), $event->getAttendees());
|
||||
}
|
||||
|
||||
protected function notifyAttendeesRemoved(AttendeesRemovedEvent $event): void {
|
||||
$this->externalSignaling->roomsDisinvited($event->getRoom(), $event->getAttendees());
|
||||
}
|
||||
|
||||
protected function notifyAttendeeRemoved(AttendeeRemovedEvent $event): void {
|
||||
$sessionIds = [];
|
||||
|
||||
$sessions = $event->getSessions();
|
||||
foreach ($sessions as $session) {
|
||||
$sessionIds[] = $session->getSessionId();
|
||||
}
|
||||
|
||||
if (!empty($sessionIds)) {
|
||||
$this->externalSignaling->roomSessionsRemoved($event->getRoom(), $sessionIds);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifySessionLeftRoom(SessionLeftRoomEvent $event): void {
|
||||
$sessionIds = [];
|
||||
if ($event->getParticipant()->getSession()) {
|
||||
// If a previous duplicated session is being removed it must be
|
||||
// notified to the external signaling server. Otherwise, only for
|
||||
// guests disconnecting is "leaving" and therefor should trigger a
|
||||
// disinvite.
|
||||
$attendeeParticipantType = $event->getParticipant()->getAttendee()->getParticipantType();
|
||||
if ($event->isRejoining()
|
||||
|| $attendeeParticipantType === Participant::GUEST
|
||||
|| $attendeeParticipantType === Participant::GUEST_MODERATOR) {
|
||||
$sessionIds[] = $event->getParticipant()->getSession()->getSessionId();
|
||||
$this->externalSignaling->roomSessionsRemoved($event->getRoom(), $sessionIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyParticipantInCallModified(AParticipantModifiedEvent $event): void {
|
||||
if ($event->getDetail(AParticipantModifiedEvent::DETAIL_IN_CALL_END_FOR_EVERYONE)) {
|
||||
// If everyone is disconnected, we will not do O(n) requests.
|
||||
// Instead, the listener of CallEndedForEveryoneEvent
|
||||
// will send all sessions to the HPB with 1 request.
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionIds = [];
|
||||
if ($event->getParticipant()->getSession()) {
|
||||
$sessionIds[] = $event->getParticipant()->getSession()->getSessionId();
|
||||
}
|
||||
|
||||
if (!empty($sessionIds)) {
|
||||
$this->externalSignaling->roomInCallChanged(
|
||||
$event->getRoom(),
|
||||
$event->getNewValue(),
|
||||
$sessionIds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyBreakoutRoomStatusModified(ARoomEvent $event): void {
|
||||
$room = $event->getRoom();
|
||||
if ($room->getBreakoutRoomStatus() === BreakoutRoom::STATUS_STARTED) {
|
||||
$this->notifyBreakoutRoomStarted($room);
|
||||
} else {
|
||||
$this->notifyBreakoutRoomStopped($room);
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyBreakoutRoomStarted(Room $room): void {
|
||||
$breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $room->getToken(), true);
|
||||
|
||||
$parentRoomParticipants = $this->participantService->getSessionsAndParticipantsForRoom($room);
|
||||
|
||||
foreach ($breakoutRooms as $breakoutRoom) {
|
||||
$sessionIds = [];
|
||||
|
||||
$breakoutRoomParticipants = $this->participantService->getParticipantsForRoom($breakoutRoom);
|
||||
foreach ($breakoutRoomParticipants as $breakoutRoomParticipant) {
|
||||
foreach ($this->getSessionIdsForNonModeratorsMatchingParticipant($breakoutRoomParticipant, $parentRoomParticipants) as $sessionId) {
|
||||
$sessionIds[] = $sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sessionIds)) {
|
||||
$this->externalSignaling->switchToRoom($room, $breakoutRoom->getToken(), $sessionIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyRoomExtended(RoomExtendedEvent $event): void {
|
||||
$room = $event->getRoom();
|
||||
|
||||
if ($room->getCallFlag() === Participant::FLAG_DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timeout = $this->timeFactory->getTime() - Session::SESSION_TIMEOUT;
|
||||
$participants = $this->participantService->getParticipantsInCall($room, $timeout);
|
||||
|
||||
$sessionIds = [];
|
||||
foreach ($participants as $participant) {
|
||||
if ($participant->getSession() instanceof Session) {
|
||||
$sessionIds[] = $participant->getSession()->getSessionId();
|
||||
}
|
||||
}
|
||||
|
||||
$newRoom = $event->getNewRoom();
|
||||
$this->externalSignaling->switchToRoom($room, $newRoom->getToken(), $sessionIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Participant $targetParticipant
|
||||
* @param Participant[] $participants
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getSessionIdsForNonModeratorsMatchingParticipant(Participant $targetParticipant, array $participants): array {
|
||||
$sessionIds = [];
|
||||
|
||||
foreach ($participants as $participant) {
|
||||
if ($participant->getAttendee()->getActorType() === $targetParticipant->getAttendee()->getActorType()
|
||||
&& $participant->getAttendee()->getActorId() === $targetParticipant->getAttendee()->getActorId()
|
||||
&& !$participant->hasModeratorPermissions()) {
|
||||
$session = $participant->getSession();
|
||||
if ($session) {
|
||||
$sessionIds[] = $session->getSessionId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sessionIds;
|
||||
}
|
||||
|
||||
protected function notifyBreakoutRoomStopped(Room $room): void {
|
||||
$breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $room->getToken(), true);
|
||||
|
||||
foreach ($breakoutRooms as $breakoutRoom) {
|
||||
$sessionIds = [];
|
||||
|
||||
$participants = $this->participantService->getSessionsAndParticipantsForRoom($breakoutRoom);
|
||||
foreach ($participants as $participant) {
|
||||
$session = $participant->getSession();
|
||||
if ($session) {
|
||||
$sessionIds[] = $session->getSessionId();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sessionIds)) {
|
||||
$this->externalSignaling->switchToRoom($breakoutRoom, $room->getToken(), $sessionIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyMessageSent(AMessageSentEvent $event): void {
|
||||
if (!$this->talkConfig->isChatRelayEnabled()) {
|
||||
if ($event instanceof ASystemMessageSentEvent && $event->shouldSkipLastActivityUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$room = $event->getRoom();
|
||||
$message = [
|
||||
'type' => 'chat',
|
||||
'chat' => [
|
||||
'refresh' => true,
|
||||
],
|
||||
];
|
||||
$this->externalSignaling->sendRoomMessage($room, $message);
|
||||
return;
|
||||
}
|
||||
|
||||
$comment = $event->getComment();
|
||||
if ($event instanceof ASystemMessageSentEvent && $event->shouldSkipLastActivityUpdate()) {
|
||||
$messageDecoded = json_decode($comment->getMessage(), true);
|
||||
$messageType = $messageDecoded['message'] ?? '';
|
||||
if ($messageType !== 'message_deleted' && $messageType !== 'message_edited') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$room = $event->getRoom();
|
||||
$data = [
|
||||
'type' => 'chat',
|
||||
'chat' => [
|
||||
'refresh' => true,
|
||||
],
|
||||
];
|
||||
|
||||
if ($event instanceof ASystemMessageSentEvent && $comment->getVerb() === ChatManager::VERB_SYSTEM && $event->shouldSkipLastActivityUpdate() === false) {
|
||||
$this->externalSignaling->sendRoomMessage($room, $data);
|
||||
return;
|
||||
}
|
||||
|
||||
$l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
|
||||
$message = $this->messageParser->createMessage($event->getRoom(), null, $comment, $l10n);
|
||||
$this->messageParser->parseMessage($message);
|
||||
if ($message->getVisibility() === false) {
|
||||
$this->externalSignaling->sendRoomMessage($room, $data);
|
||||
return;
|
||||
}
|
||||
|
||||
$thread = null;
|
||||
if (!isset($messageType)) {
|
||||
$threadId = (int)$comment->getTopmostParentId() ?: $comment->getId();
|
||||
try {
|
||||
$thread = $this->threadService->findByThreadId($room->getId(), (int)$threadId);
|
||||
} catch (DoesNotExistException) {
|
||||
}
|
||||
}
|
||||
|
||||
$data['chat']['comment'] = $message->toArray('json', $thread);
|
||||
|
||||
if ($event instanceof ASystemMessageSentEvent && $event->getParent() !== null) {
|
||||
$parent = $event->getParent();
|
||||
$parentMessage = $this->messageParser->createMessage($event->getRoom(), null, $parent, $l10n);
|
||||
$this->messageParser->parseMessage($parentMessage);
|
||||
$data['chat']['comment']['parent'] = $parentMessage->toArray('json', $thread);
|
||||
}
|
||||
|
||||
$this->externalSignaling->sendRoomMessage($room, $data);
|
||||
}
|
||||
|
||||
protected function notifyReactionSent(AReactionEvent $event): void {
|
||||
if (!$this->talkConfig->isChatRelayEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$room = $event->getRoom();
|
||||
$data = [
|
||||
'type' => 'chat',
|
||||
'chat' => [
|
||||
'refresh' => true,
|
||||
],
|
||||
];
|
||||
|
||||
$comment = $event->getMessage();
|
||||
$messageType = $event instanceof ReactionAddedEvent ? ChatManager::VERB_REACTION : 'reaction_revoked';
|
||||
|
||||
$threadId = (int)$comment->getTopmostParentId() ?: $comment->getId();
|
||||
try {
|
||||
$thread = $this->threadService->findByThreadId($room->getId(), (int)$threadId);
|
||||
} catch (DoesNotExistException) {
|
||||
$thread = null;
|
||||
}
|
||||
|
||||
$reactions = $comment->getReactions();
|
||||
if ($event instanceof ReactionRemovedEvent) {
|
||||
if (array_key_exists($event->getReaction(), $reactions) && $reactions[$event->getReaction()] > 1) {
|
||||
--$reactions[$event->getReaction()];
|
||||
} else {
|
||||
unset($reactions[$event->getReaction()]);
|
||||
}
|
||||
} elseif ($event instanceof ReactionAddedEvent) {
|
||||
if (array_key_exists($event->getReaction(), $reactions)) {
|
||||
++$reactions[$event->getReaction()];
|
||||
} else {
|
||||
$reactions[$event->getReaction()] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$comment->setReactions($reactions);
|
||||
$l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
|
||||
$message = $this->messageParser->createMessage($event->getRoom(), null, $comment, $l10n);
|
||||
$this->messageParser->parseMessage($message);
|
||||
// Build reaction message data
|
||||
$data['chat']['comment'] = [
|
||||
'id' => $event->getReactionMessage()?->getId(),
|
||||
'token' => $event->getRoom()->getToken(),
|
||||
'actorType' => $event->getActorType(),
|
||||
'actorId' => $event->getActorId(),
|
||||
'actorDisplayName' => $event->getActorDisplayName(),
|
||||
'timestamp' => $this->timeFactory->getTime(),
|
||||
'message' => $event->getReaction(),
|
||||
'messageParameters' => [],
|
||||
'systemMessage' => $messageType,
|
||||
'messageType' => ChatManager::VERB_SYSTEM,
|
||||
'isReplyable' => false,
|
||||
'referenceId' => '',
|
||||
'reactions' => [],
|
||||
'markdown' => false ,
|
||||
'expirationTimestamp' => $message->getExpirationDateTime()?->getTimestamp(), // base on parent post timestamp + room expiration
|
||||
'threadId' => $threadId,
|
||||
];
|
||||
|
||||
$data['chat']['comment']['parent'] = $message->toArray('json', $thread);
|
||||
$this->externalSignaling->sendRoomMessage($room, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Signaling;
|
||||
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use OCA\Talk\CachePrefix;
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\CertificateService;
|
||||
use OCA\Talk\Service\RoomService;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IConfig;
|
||||
|
||||
class Manager {
|
||||
public const FEATURE_HEADER = 'X-Spreed-Signaling-Features';
|
||||
|
||||
protected ICache $cache;
|
||||
|
||||
public function __construct(
|
||||
protected IConfig $serverConfig,
|
||||
protected Config $talkConfig,
|
||||
protected RoomService $roomService,
|
||||
protected ITimeFactory $timeFactory,
|
||||
protected IClientService $clientService,
|
||||
protected CertificateService $certificateService,
|
||||
ICacheFactory $cacheFactory,
|
||||
) {
|
||||
$this->cache = $cacheFactory->createDistributed(CachePrefix::SIGNALING_ASSIGNED_SERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $serverId
|
||||
* @return array{status: Http::STATUS_OK, data: array{version: string, warning?: string, features?: non-empty-list<string>}}|array{status: Http::STATUS_INTERNAL_SERVER_ERROR, data: array{error: string, version?: string}}
|
||||
* @throws \OutOfBoundsException When the serverId is not found
|
||||
*/
|
||||
public function checkServerCompatibility(int $serverId): array {
|
||||
$signalingServers = $this->talkConfig->getSignalingServers();
|
||||
if (empty($signalingServers) || !isset($signalingServers[$serverId])) {
|
||||
throw new \OutOfBoundsException();
|
||||
}
|
||||
|
||||
$url = rtrim($signalingServers[$serverId]['server'], '/');
|
||||
$url = strtolower($url);
|
||||
|
||||
if (str_starts_with($url, 'wss://')) {
|
||||
$url = 'https://' . substr($url, 6);
|
||||
}
|
||||
|
||||
if (str_starts_with($url, 'ws://')) {
|
||||
$url = 'http://' . substr($url, 5);
|
||||
}
|
||||
|
||||
$verifyServer = (bool)$signalingServers[$serverId]['verify'];
|
||||
|
||||
if ($verifyServer && str_contains($url, 'https://')) {
|
||||
$expiration = $this->certificateService->getCertificateExpirationInDays($url);
|
||||
|
||||
if ($expiration < 0) {
|
||||
return [
|
||||
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
|
||||
'data' => [
|
||||
'error' => 'CERTIFICATE_EXPIRED',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$client = $this->clientService->newClient();
|
||||
try {
|
||||
$timeBefore = $this->timeFactory->getTime();
|
||||
$response = $client->get($url . '/api/v1/welcome', [
|
||||
'verify' => $verifyServer,
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => true,
|
||||
],
|
||||
]);
|
||||
$timeAfter = $this->timeFactory->getTime();
|
||||
|
||||
$body = $response->getBody();
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
return [
|
||||
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
|
||||
'data' => [
|
||||
'error' => 'JSON_INVALID',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($data['version'])) {
|
||||
return [
|
||||
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
|
||||
'data' => [
|
||||
'error' => 'UPDATE_REQUIRED',
|
||||
'version' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->isCompatibleSignalingServer($response)) {
|
||||
return [
|
||||
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
|
||||
'data' => [
|
||||
'error' => 'UPDATE_REQUIRED',
|
||||
'version' => $data['version'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$responseTime = $this->timeFactory->getDateTime($response->getHeader('date'))->getTimestamp();
|
||||
if (($timeBefore - Config::ALLOWED_BACKEND_TIMEOFFSET) > $responseTime
|
||||
|| ($timeAfter + Config::ALLOWED_BACKEND_TIMEOFFSET) < $responseTime) {
|
||||
return [
|
||||
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
|
||||
'data' => [
|
||||
'error' => 'TIME_OUT_OF_SYNC',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$missingFeatures = $this->getSignalingServerMissingFeatures($response);
|
||||
if (!empty($missingFeatures)) {
|
||||
return [
|
||||
'status' => Http::STATUS_OK,
|
||||
'data' => [
|
||||
'warning' => 'UPDATE_OPTIONAL',
|
||||
'features' => $missingFeatures,
|
||||
'version' => $data['version'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => Http::STATUS_OK,
|
||||
'data' => [
|
||||
'version' => $data['version'],
|
||||
],
|
||||
];
|
||||
} catch (ConnectException) {
|
||||
return [
|
||||
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
|
||||
'data' => [
|
||||
'error' => 'CAN_NOT_CONNECT',
|
||||
],
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
|
||||
'data' => [
|
||||
'error' => (string)$e->getCode(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function isCompatibleSignalingServer(IResponse $response): bool {
|
||||
$featureHeader = $response->getHeader(self::FEATURE_HEADER);
|
||||
$features = explode(',', $featureHeader);
|
||||
$features = array_map('trim', $features);
|
||||
return in_array('audio-video-permissions', $features, true)
|
||||
&& in_array('federation', $features, true)
|
||||
&& in_array('incall-all', $features, true)
|
||||
&& in_array('hello-v2', $features, true)
|
||||
&& in_array('switchto', $features, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getSignalingServerMissingFeatures(IResponse $response): array {
|
||||
$featureHeader = $response->getHeader(self::FEATURE_HEADER);
|
||||
$features = explode(',', $featureHeader);
|
||||
$features = array_map('trim', $features);
|
||||
|
||||
$optionFeatures = [
|
||||
'dialout',
|
||||
'join-features',
|
||||
];
|
||||
|
||||
if ($this->talkConfig->hasExperiment(Config::EXPERIMENTAL_CHAT_RELAY)) {
|
||||
$optionFeatures[] = 'chat-relay';
|
||||
}
|
||||
|
||||
return array_values(array_diff($optionFeatures, $features));
|
||||
}
|
||||
|
||||
public function getSignalingServerLinkForConversation(?Room $room): string {
|
||||
if ($this->talkConfig->getSignalingMode() === Config::SIGNALING_INTERNAL) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->getSignalingServerForConversation($room)['server'];
|
||||
}
|
||||
|
||||
public function getSignalingServerForConversation(?Room $room): array {
|
||||
switch ($this->talkConfig->getSignalingMode()) {
|
||||
case Config::SIGNALING_EXTERNAL:
|
||||
return $this->getSignalingServerRandomly();
|
||||
case Config::SIGNALING_CLUSTER_CONVERSATION:
|
||||
if (!$room instanceof Room) {
|
||||
throw new \RuntimeException('Can not get conversation cluster HPB without conversation');
|
||||
}
|
||||
return $this->getSignalingServerConversationCluster($room);
|
||||
default:
|
||||
throw new \RuntimeException('Unsupported signaling mode');
|
||||
}
|
||||
}
|
||||
|
||||
public function getSignalingServerRandomly(): array {
|
||||
$servers = $this->talkConfig->getSignalingServers();
|
||||
try {
|
||||
$serverId = random_int(0, count($servers) - 1);
|
||||
return $servers[$serverId];
|
||||
} catch (\Exception $e) {
|
||||
return $servers[0];
|
||||
}
|
||||
}
|
||||
|
||||
public function getSignalingServerConversationCluster(Room $room): array {
|
||||
$serverId = $room->getAssignedSignalingServer();
|
||||
$servers = $this->talkConfig->getSignalingServers();
|
||||
|
||||
if ($serverId !== null && isset($servers[$serverId])) {
|
||||
return $servers[$serverId];
|
||||
}
|
||||
|
||||
try {
|
||||
$serverIdToAssign = random_int(0, count($servers) - 1);
|
||||
} catch (\Exception $e) {
|
||||
$serverIdToAssign = 0;
|
||||
}
|
||||
|
||||
$hardcodedServers = $this->serverConfig->getSystemValue('talk_hardcoded_hpb', []);
|
||||
if (isset($hardcodedServers[$room->getToken()])) {
|
||||
$hardcodedServerId = $hardcodedServers[$room->getToken()];
|
||||
if (isset($servers[$hardcodedServerId])) {
|
||||
$serverIdToAssign = $hardcodedServerId;
|
||||
}
|
||||
}
|
||||
|
||||
$serverId = $this->cache->get($room->getToken());
|
||||
if ($serverId === null) {
|
||||
$this->cache->set($room->getToken(), $serverIdToAssign);
|
||||
$serverId = $serverIdToAssign;
|
||||
$this->roomService->setAssignedSignalingServer($room, $serverId);
|
||||
}
|
||||
|
||||
return $servers[$serverId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Signaling;
|
||||
|
||||
use OCA\Talk\Model\Session;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCP\AppFramework\Db\TTransactional;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class Messages {
|
||||
use TTransactional;
|
||||
|
||||
public function __construct(
|
||||
protected IDBConnection $db,
|
||||
protected ParticipantService $participantService,
|
||||
protected ITimeFactory $timeFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $sessionIds
|
||||
*/
|
||||
public function deleteMessages(array $sessionIds): void {
|
||||
$delete = $this->db->getQueryBuilder();
|
||||
$delete->delete('talk_internalsignaling')
|
||||
->where($delete->expr()->in('recipient', $delete->createNamedParameter($sessionIds, IQueryBuilder::PARAM_STR_ARRAY)))
|
||||
->orWhere($delete->expr()->in('sender', $delete->createNamedParameter($sessionIds, IQueryBuilder::PARAM_STR_ARRAY)));
|
||||
|
||||
$this->atomic(function () use ($delete): void {
|
||||
$delete->executeStatement();
|
||||
}, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $senderSessionId
|
||||
* @param string $recipientSessionId
|
||||
* @param string $message
|
||||
*/
|
||||
public function addMessage(string $senderSessionId, string $recipientSessionId, string $message): void {
|
||||
$insert = $this->db->getQueryBuilder();
|
||||
$insert->insert('talk_internalsignaling')
|
||||
->values(
|
||||
[
|
||||
'sender' => $insert->createNamedParameter($senderSessionId),
|
||||
'recipient' => $insert->createNamedParameter($recipientSessionId),
|
||||
'timestamp' => $insert->createNamedParameter($this->timeFactory->getTime()),
|
||||
'message' => $insert->createNamedParameter($message),
|
||||
]
|
||||
);
|
||||
$insert->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param string $message
|
||||
*/
|
||||
public function addMessageForAllParticipants(Room $room, string $message): void {
|
||||
$insert = $this->db->getQueryBuilder();
|
||||
$insert->insert('talk_internalsignaling')
|
||||
->values(
|
||||
[
|
||||
'sender' => $insert->createParameter('sender'),
|
||||
'recipient' => $insert->createParameter('recipient'),
|
||||
'timestamp' => $insert->createNamedParameter($this->timeFactory->getTime()),
|
||||
'message' => $insert->createNamedParameter($message),
|
||||
]
|
||||
);
|
||||
|
||||
$participants = $this->participantService->getParticipantsForAllSessions($room);
|
||||
$this->atomic(function () use ($participants, $insert): void {
|
||||
foreach ($participants as $participant) {
|
||||
$session = $participant->getSession();
|
||||
if ($session instanceof Session) {
|
||||
$insert->setParameter('sender', $session->getSessionId())
|
||||
->setParameter('recipient', $session->getSessionId())
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
}, $this->db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages and delete them afterwards
|
||||
*
|
||||
* To make sure we don't delete messages which we didn't return
|
||||
* we do it with 1 second difference. This means you don't receive messages
|
||||
* immediately, but the next polling is only 1 second later and will get the
|
||||
* "new" message.
|
||||
*
|
||||
* @param string $sessionId
|
||||
* @return list<array{type: string, data: string}>
|
||||
*/
|
||||
public function getAndDeleteMessages(string $sessionId): array {
|
||||
$messages = [];
|
||||
$time = $this->timeFactory->getTime() - 1;
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('*')
|
||||
->from('talk_internalsignaling')
|
||||
->where($query->expr()->eq('recipient', $query->createNamedParameter($sessionId)))
|
||||
->andWhere($query->expr()->lte('timestamp', $query->createNamedParameter($time)))
|
||||
->orderBy('id', 'ASC');
|
||||
|
||||
$delete = $this->db->getQueryBuilder();
|
||||
$delete->delete('talk_internalsignaling')
|
||||
->where($delete->expr()->eq('recipient', $delete->createNamedParameter($sessionId)))
|
||||
->andWhere($delete->expr()->lte('timestamp', $delete->createNamedParameter($time)));
|
||||
|
||||
$this->atomic(function () use (&$messages, $query, $delete): void {
|
||||
$result = $query->executeQuery();
|
||||
|
||||
while ($row = $result->fetch()) {
|
||||
$messages[] = ['type' => 'message', 'data' => $row['message']];
|
||||
}
|
||||
$result->closeCursor();
|
||||
|
||||
$delete->executeStatement();
|
||||
}, $this->db);
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expires all signaling messages that are too old or invalid
|
||||
*
|
||||
* @param int $olderThan
|
||||
*/
|
||||
public function expireOlderThan(int $olderThan): void {
|
||||
$time = $this->timeFactory->getTime() - $olderThan;
|
||||
|
||||
$delete = $this->db->getQueryBuilder();
|
||||
$delete->delete('talk_internalsignaling')
|
||||
->where($delete->expr()->lt('timestamp', $delete->createNamedParameter($time)));
|
||||
|
||||
$this->atomic(function () use ($delete): void {
|
||||
$delete->executeStatement();
|
||||
}, $this->db);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Signaling\Responses;
|
||||
|
||||
/**
|
||||
* Success:
|
||||
* ```
|
||||
* {
|
||||
* "callid": "the-call-id"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Error:
|
||||
* ```
|
||||
* "error": {
|
||||
* "code": "error-code",
|
||||
* "message": "Human readable error.",
|
||||
* "details": {
|
||||
* ...optional-details-object...
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
final class DialOut {
|
||||
public function __construct(
|
||||
/** @var non-empty-string|null */
|
||||
public ?string $callId = null,
|
||||
public ?DialOutError $error = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Signaling\Responses;
|
||||
|
||||
/**
|
||||
* Error:
|
||||
* ```
|
||||
* {
|
||||
* "code": "error-code",
|
||||
* "message": "Human readable error.",
|
||||
* "details": {
|
||||
* ...optional-details-object...
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
final class DialOutError {
|
||||
public function __construct(
|
||||
/** @var non-empty-string */
|
||||
public ?string $code,
|
||||
public ?string $message = null,
|
||||
/** @var ?array{attendeeId: int} */
|
||||
public ?array $details = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Signaling\Responses;
|
||||
|
||||
final class Response {
|
||||
public function __construct(
|
||||
/** @var non-empty-string */
|
||||
public string $type,
|
||||
/** @var DialOut|null */
|
||||
public ?DialOut $dialOut,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user