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

Источник: 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:
2026-07-06 14:07:50 +00:00
commit 01acfa3b40
1716 changed files with 613013 additions and 0 deletions
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Model\Invitation;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\Files\SimpleFS\InMemoryFile;
/**
* @psalm-import-type TalkChatMentionSuggestion from ResponseDefinitions
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
*/
class AvatarController {
public function __construct(
protected ProxyRequest $proxy,
) {
}
/**
* @see \OCA\Talk\Controller\AvatarController::getAvatar()
*
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>
* @throws CannotReachRemoteException
*
* 200: Room avatar returned
*/
public function getAvatar(Room $room, ?Participant $participant, ?Invitation $invitation, bool $darkTheme): FileDisplayResponse {
if ($participant === null && $invitation === null) {
throw new CannotReachRemoteException('Must receive either participant or invitation');
}
$proxy = $this->proxy->get(
$participant ? $participant->getAttendee()->getInvitedCloudId() : $invitation->getLocalCloudId(),
$participant ? $participant->getAttendee()->getAccessToken() : $invitation->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/room/' . $room->getRemoteToken() . '/avatar' . ($darkTheme ? '/dark' : ''),
);
if ($proxy->getStatusCode() !== Http::STATUS_OK) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode(), (string)$proxy->getBody());
throw new CannotReachRemoteException('Avatar request had unexpected status code');
}
$content = $proxy->getBody();
if ($content === '') {
throw new CannotReachRemoteException('No avatar content received');
}
$file = new InMemoryFile($room->getToken(), $content);
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $file->getMimeType()]);
// Cache for 1 day
$response->cacheFor(60 * 60 * 24, false, true);
return $response;
}
/**
* @see \OCA\Talk\Controller\AvatarController::getUserProxyAvatar()
*
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>
* @throws CannotReachRemoteException
*
* 200: User avatar returned
*/
public function getUserProxyAvatar(string $remoteServer, string $user, int $size, bool $darkTheme): FileDisplayResponse {
$proxy = $this->proxy->get(
null,
null,
$remoteServer . '/index.php/avatar/' . $user . '/' . $size . ($darkTheme ? '/dark' : ''),
);
if ($proxy->getStatusCode() !== Http::STATUS_OK) {
if ($proxy->getStatusCode() !== Http::STATUS_NOT_FOUND) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode(), (string)$proxy->getBody());
}
throw new CannotReachRemoteException('Avatar request had unexpected status code');
}
$content = $proxy->getBody();
if ($content === '') {
throw new CannotReachRemoteException('No avatar content received');
}
$file = new InMemoryFile($user, $content);
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $file->getMimeType()]);
// Cache for 1 day
$response->cacheFor(60 * 60 * 24, false, true);
return $response;
}
}
@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Model\Session;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
/**
* @psalm-import-type TalkCapabilities from ResponseDefinitions
* @psalm-import-type TalkCallPeer from ResponseDefinitions
* @psalm-import-type TalkParticipant from ResponseDefinitions
* @psalm-import-type TalkRoom from ResponseDefinitions
*/
class CallController {
public function __construct(
protected ProxyRequest $proxy,
protected UserConverter $userConverter,
) {
}
/**
* @see \OCA\Talk\Controller\CallController::getPeersForCall()
*
* @param Room $room the federated room to get the call peers
* @param Participant $participant the federated user to get the call peers
* @return DataResponse<Http::STATUS_OK, list<TalkCallPeer>, array{}>
* @throws CannotReachRemoteException
*
* 200: List of peers in the call returned
*/
public function getPeersForCall(Room $room, Participant $participant): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken(),
);
/** @var list<TalkCallPeer> $data */
$data = $this->proxy->getOCSData($proxy);
/** @var list<TalkCallPeer> $data */
$data = $this->userConverter->convertAttendees($room, $data, 'actorType', 'actorId', 'displayName');
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
return new DataResponse($data, $statusCode);
}
/**
* @see \OCA\Talk\Controller\CallController::joinFederatedCall()
*
* @param Room $room the federated room to join the call in
* @param Participant $participant the federated user that will join the
* call; the participant must have a session
* @param int<0, 15> $flags In-Call flags
* @psalm-param int-mask-of<Participant::FLAG_*> $flags
* @param bool $silent Join the call silently
* @param bool $recordingConsent Agreement to be recorded
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Federated user is now in the call
* 400: Conditions to join not met
* 404: Room not found
*/
public function joinFederatedCall(Room $room, Participant $participant, int $flags, bool $silent, bool $recordingConsent): DataResponse {
$options = [
'sessionId' => $participant->getSession()->getSessionId(),
'flags' => $flags,
'silent' => $silent,
'recordingConsent' => $recordingConsent,
];
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation',
$options,
);
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
if ($statusCode === Http::STATUS_BAD_REQUEST) {
/** @var array{error: string} $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]);
return new DataResponse($data, $statusCode);
}
return new DataResponse(null, $statusCode);
}
/**
* @see \OCA\Talk\Controller\CallController::ringAttendee()
*
* @param int $attendeeId ID of the attendee to ring
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Attendee rang successfully
* 400: Ringing attendee is not possible
* 404: Attendee could not be found
*/
public function ringAttendee(Room $room, Participant $participant, int $attendeeId): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/ring/' . $attendeeId,
);
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
if ($statusCode === Http::STATUS_BAD_REQUEST) {
/** @var array{error: string} $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]);
return new DataResponse($data, $statusCode);
}
return new DataResponse(null, $statusCode);
}
/**
* @see \OCA\Talk\Controller\CallController::updateFederatedCallFlags()
*
* @param Room $room the federated room to update the call flags in
* @param Participant $participant the federated user to update the call
* flags; the participant must have a session
* @param int<0, 15> $flags New flags
* @psalm-param int-mask-of<Participant::FLAG_*> $flags New flags
* @return DataResponse<Http::STATUS_OK|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, null, array{}>
* @throws CannotReachRemoteException
*
* 200: In-call flags updated successfully for federated user
* 400: Updating in-call flags is not possible
* 404: Room not found
*/
public function updateFederatedCallFlags(Room $room, Participant $participant, int $flags): DataResponse {
$options = [
'sessionId' => $participant->getSession()->getSessionId(),
'flags' => $flags,
];
$proxy = $this->proxy->put(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation',
$options,
);
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_BAD_REQUEST, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
return new DataResponse(null, $statusCode);
}
/**
* @see \OCA\Talk\Controller\CallController::leaveFederatedCall()
*
* @param Room $room the federated room to leave the call in
* @param Participant $participant the federated user that will leave the
* call; the participant must have a session
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, null, array{}>
* @throws CannotReachRemoteException
*
* 200: Federated user left the call
* 404: Room not found
*/
public function leaveFederatedCall(Room $room, Participant $participant): DataResponse {
$options = [
'sessionId' => $participant->getSession()->getSessionId(),
];
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/call/' . $room->getRemoteToken() . '/federation',
$options,
);
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
return new DataResponse(null, $statusCode);
}
}
@@ -0,0 +1,539 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\CachePrefix;
use OCA\Talk\Chat\Notifier;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RoomFormatter;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\ICache;
use OCP\ICacheFactory;
/**
* @psalm-import-type TalkChatMentionSuggestion from ResponseDefinitions
* @psalm-import-type TalkChatMessage from ResponseDefinitions
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
* @psalm-import-type TalkRoom from ResponseDefinitions
*/
class ChatController {
protected ?ICache $proxyCacheMessages;
public function __construct(
protected ProxyRequest $proxy,
protected UserConverter $userConverter,
protected ParticipantService $participantService,
protected RoomFormatter $roomFormatter,
protected Notifier $notifier,
ICacheFactory $cacheFactory,
) {
$this->proxyCacheMessages = $cacheFactory->isAvailable() ? $cacheFactory->createDistributed(CachePrefix::FEDERATED_PCM) : null;
}
/**
* @see \OCA\Talk\Controller\ChatController::sendMessage()
*
* @return DataResponse<Http::STATUS_CREATED, ?TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_REQUEST_ENTITY_TOO_LARGE|Http::STATUS_TOO_MANY_REQUESTS, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 201: Message sent successfully
* 400: Sending message is not possible
* 404: Actor not found
* 413: Message too long
* 429: Mention rate limit exceeded (guests only)
*/
public function sendMessage(Room $room, Participant $participant, string $message, string $referenceId, int $replyTo, bool $silent, string $threadTitle, int $threadId): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken(),
[
'message' => $message,
'actorDisplayName' => $participant->getAttendee()->getDisplayName(),
'referenceId' => $referenceId,
'replyTo' => $replyTo,
'silent' => $silent,
'threadTitle' => $threadTitle,
'threadId' => $threadId,
],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_CREATED) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_NOT_FOUND,
Http::STATUS_REQUEST_ENTITY_TOO_LARGE,
Http::STATUS_TOO_MANY_REQUESTS,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
/** @var array{error: string} $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_CREATED]);
return new DataResponse($data, $statusCode);
}
/** @var ?TalkChatMessageWithParent $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_CREATED]);
if (!empty($data)) {
$data = $this->userConverter->convertMessage($room, $data);
} else {
$data = null;
}
$headers = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$headers['X-Chat-Last-Common-Read'] = (string)(int)$proxy->getHeader('X-Chat-Last-Common-Read');
}
return new DataResponse(
$data,
Http::STATUS_CREATED,
$headers,
);
}
/**
* @return DataResponse<Http::STATUS_OK, list<TalkChatMessageWithParent>, array{'X-Chat-Last-Common-Read'?: numeric-string, X-Chat-Last-Given?: numeric-string}>|DataResponse<Http::STATUS_NOT_MODIFIED, null, array{}>
* @throws CannotReachRemoteException
*
* 200: Messages returned
* 304: No messages
*
* @see \OCA\Talk\Controller\ChatController::getMessageContext()
*/
public function receiveMessages(
Room $room,
Participant $participant,
int $lookIntoFuture,
int $limit,
int $lastKnownMessageId,
int $lastCommonReadId,
int $timeout,
int $setReadMarker,
int $includeLastKnown,
int $noStatusUpdate,
int $markNotificationsAsRead): DataResponse {
$cacheKey = sha1(json_encode([$room->getRemoteServer(), $room->getRemoteToken()]));
if ($lookIntoFuture && $markNotificationsAsRead && $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
$this->notifier->markMentionNotificationsRead($room, $participant->getAttendee()->getActorId());
}
if ($lookIntoFuture) {
if ($this->proxyCacheMessages instanceof ICache) {
for ($i = 0; $i <= $timeout; $i++) {
$cacheData = (int)$this->proxyCacheMessages->get($cacheKey);
if ($lastKnownMessageId !== $cacheData) {
break;
}
sleep(1);
}
} else {
// Poor-mans timeout, should later on cancel/trigger earlier,
// by checking the PCM database table
sleep(max(0, $timeout - 5));
}
}
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken(),
[
'lookIntoFuture' => $lookIntoFuture,
'limit' => $limit,
'lastKnownMessageId' => $lastKnownMessageId,
'lastCommonReadId' => $lastCommonReadId,
'timeout' => 0,
'setReadMarker' => $setReadMarker,
'includeLastKnown' => $includeLastKnown,
'noStatusUpdate' => $noStatusUpdate,
'markNotificationsAsRead' => $markNotificationsAsRead,
],
);
if ($lookIntoFuture && $setReadMarker) {
$this->participantService->updateUnreadInfoForProxyParticipant($participant,
0,
false,
false,
(int)($proxy->getHeader('X-Chat-Last-Given') ?: $lastKnownMessageId),
);
}
if ($proxy->getStatusCode() === Http::STATUS_NOT_MODIFIED) {
if ($lookIntoFuture && $this->proxyCacheMessages instanceof ICache) {
$cacheData = $this->proxyCacheMessages->get($cacheKey);
if ($cacheData === null || $cacheData < $lastKnownMessageId) {
$this->proxyCacheMessages->set($cacheKey, $lastKnownMessageId, 300);
}
}
return new DataResponse(null, Http::STATUS_NOT_MODIFIED);
}
$headers = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$headers['X-Chat-Last-Common-Read'] = (string)(int)$proxy->getHeader('X-Chat-Last-Common-Read');
}
if ($proxy->getHeader('X-Chat-Last-Given')) {
$headers['X-Chat-Last-Given'] = (string)(int)$proxy->getHeader('X-Chat-Last-Given');
if ($lookIntoFuture && $this->proxyCacheMessages instanceof ICache) {
$cacheData = $this->proxyCacheMessages->get($cacheKey);
if ($cacheData === null || $cacheData < $headers['X-Chat-Last-Given']) {
$this->proxyCacheMessages->set($cacheKey, (int)$headers['X-Chat-Last-Given'], 300);
}
}
}
/** @var list<TalkChatMessageWithParent> $data */
$data = $this->proxy->getOCSData($proxy);
/** @var list<TalkChatMessageWithParent> $data */
$data = $this->userConverter->convertMessages($room, $data);
return new DataResponse($data, Http::STATUS_OK, $headers);
}
/**
* @return DataResponse<Http::STATUS_OK, list<TalkChatMessageWithParent>, array{'X-Chat-Last-Common-Read'?: numeric-string, X-Chat-Last-Given?: numeric-string}>|DataResponse<Http::STATUS_NOT_MODIFIED, null, array{}>
* @throws CannotReachRemoteException
*
* 200: Message context returned
* 304: No messages
*
* @see \OCA\Talk\Controller\ChatController::getMessageContext()
*/
public function getMessageContext(Room $room, Participant $participant, int $messageId, int $limit): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/' . $messageId . '/context',
[
'limit' => $limit,
],
);
if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
$this->notifier->markMentionNotificationsRead($room, $participant->getAttendee()->getActorId());
}
if ($proxy->getStatusCode() === Http::STATUS_NOT_MODIFIED) {
return new DataResponse(null, Http::STATUS_NOT_MODIFIED);
}
$headers = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$headers['X-Chat-Last-Common-Read'] = (string)(int)$proxy->getHeader('X-Chat-Last-Common-Read');
}
if ($proxy->getHeader('X-Chat-Last-Given')) {
$headers['X-Chat-Last-Given'] = (string)(int)$proxy->getHeader('X-Chat-Last-Given');
}
/** @var list<TalkChatMessageWithParent> $data */
$data = $this->proxy->getOCSData($proxy);
/** @var list<TalkChatMessageWithParent> $data */
$data = $this->userConverter->convertMessages($room, $data);
return new DataResponse($data, Http::STATUS_OK, $headers);
}
/**
* @return DataResponse<Http::STATUS_OK, array<string, list<TalkChatMessage>>, array{}>
* @throws CannotReachRemoteException
*
* 200: List of shared objects messages of each type returned
*
* @see \OCA\Talk\Controller\ChatController::getObjectsSharedInRoomOverview()
*/
public function getObjectsSharedInRoomOverview(Room $room, Participant $participant, int $limit): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/share/overview',
[
'limit' => $limit,
],
);
// We allow "200 OK" and "406 Not Acceptable" here, so that 33 against 32 and before is working well
/** @var array<string, list<TalkChatMessage>> $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_NOT_ACCEPTABLE]);
$result = [];
foreach ($data as $type => $items) {
$result[$type] = array_values($this->userConverter->convertMessages($room, $items));
}
/** @var array<string, list<TalkChatMessage>> $result */
return new DataResponse($result, Http::STATUS_OK);
}
/**
* @return DataResponse<Http::STATUS_OK, array<string, TalkChatMessage>, array{}>
* @throws CannotReachRemoteException
*
* 200: List of shared objects messages returned
*
* @see \OCA\Talk\Controller\ChatController::getObjectsSharedInRoom()
*/
public function getObjectsSharedInRoom(Room $room, Participant $participant, string $objectType, int $lastKnownMessageId, int $limit): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/share',
[
'objectType' => $objectType,
'lastKnownMessageId' => $lastKnownMessageId,
'limit' => $limit,
],
);
// We allow "200 OK" and "406 Not Acceptable" here, so that 33 against 32 and before is working well
/** @var array<string, TalkChatMessage> $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_NOT_ACCEPTABLE]);
/** @var array<string, TalkChatMessage> $data */
$data = $this->userConverter->convertMessages($room, $data);
return new DataResponse($data, Http::STATUS_OK);
}
/**
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED, TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_METHOD_NOT_ALLOWED|Http::STATUS_REQUEST_ENTITY_TOO_LARGE, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Message edited successfully
* 202: Message edited successfully, but a bot or Matterbridge is configured, so the information can be replicated to other services
* 400: Editing message is not possible, e.g. when the new message is empty or the message is too old
* 403: Missing permissions to edit message
* 404: Message not found
* 405: Editing this message type is not allowed
*
* @see \OCA\Talk\Controller\ChatController::editMessage()
*/
public function editMessage(Room $room, Participant $participant, int $messageId, string $message): DataResponse {
$proxy = $this->proxy->put(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/' . $messageId,
[
'message' => $message,
],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK && $statusCode !== Http::STATUS_ACCEPTED) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_FORBIDDEN,
Http::STATUS_NOT_FOUND,
Http::STATUS_METHOD_NOT_ALLOWED,
Http::STATUS_REQUEST_ENTITY_TOO_LARGE,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
$data = ['error' => 'status'];
} elseif ($statusCode === Http::STATUS_BAD_REQUEST) {
/** @var array{error: string} $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]);
} else {
$data = [];
}
return new DataResponse($data, $statusCode);
}
/** @var TalkChatMessageWithParent $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_ACCEPTED]);
$data = $this->userConverter->convertMessage($room, $data);
$headers = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$headers['X-Chat-Last-Common-Read'] = (string)(int)$proxy->getHeader('X-Chat-Last-Common-Read');
}
return new DataResponse(
$data,
$statusCode,
$headers,
);
}
/**
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED, TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_METHOD_NOT_ALLOWED, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Message deleted successfully
* 202: Message deleted successfully, but a bot or Matterbridge is configured, so the information can be replicated elsewhere
* 400: Deleting message is not possible
* 403: Missing permissions to delete message
* 404: Message not found
* 405: Deleting this message type is not allowed
*
* @see \OCA\Talk\Controller\ChatController::deleteMessage()
*/
public function deleteMessage(Room $room, Participant $participant, int $messageId): DataResponse {
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/' . $messageId,
);
/** @var Http::STATUS_OK|Http::STATUS_ACCEPTED|Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_REQUEST_ENTITY_TOO_LARGE $statusCode */
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK && $statusCode !== Http::STATUS_ACCEPTED) {
if (in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_FORBIDDEN,
Http::STATUS_NOT_FOUND,
Http::STATUS_REQUEST_ENTITY_TOO_LARGE,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
return new DataResponse([], $statusCode);
}
/** @var TalkChatMessageWithParent $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_ACCEPTED]);
$data = $this->userConverter->convertMessage($room, $data);
$headers = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$headers['X-Chat-Last-Common-Read'] = (string)(int)$proxy->getHeader('X-Chat-Last-Common-Read');
}
return new DataResponse(
$data,
$statusCode,
$headers,
);
}
/**
* @see \OCA\Talk\Controller\ChatController::setReadMarker()
*
* @param 'json'|'xml' $responseFormat
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{X-Chat-Last-Common-Read?: numeric-string}>
* @throws CannotReachRemoteException
*
* 200: List of mention suggestions returned
*/
public function setReadMarker(Room $room, Participant $participant, string $responseFormat, ?int $lastReadMessage): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/read',
$lastReadMessage !== null ? [
'lastReadMessage' => $lastReadMessage,
] : [],
);
/** @var TalkRoom $data */
$data = $this->proxy->getOCSData($proxy);
$this->participantService->updateUnreadInfoForProxyParticipant(
$participant,
$data['unreadMessages'],
$data['unreadMention'],
$data['unreadMentionDirect'],
$data['lastReadMessage'],
);
$headers = $lastCommonRead = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$lastCommonRead[$room->getId()] = (int)$proxy->getHeader('X-Chat-Last-Common-Read');
$headers['X-Chat-Last-Common-Read'] = (string)$lastCommonRead[$room->getId()];
}
return new DataResponse($this->roomFormatter->formatRoom(
$responseFormat,
$lastCommonRead,
$room,
$participant,
), Http::STATUS_OK, $headers);
}
/**
* @see \OCA\Talk\Controller\ChatController::markUnread()
*
* @param 'json'|'xml' $responseFormat
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{X-Chat-Last-Common-Read?: numeric-string}>
* @throws CannotReachRemoteException
*
* 200: List of mention suggestions returned
*/
public function markUnread(Room $room, Participant $participant, string $responseFormat): DataResponse {
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/read',
);
/** @var TalkRoom $data */
$data = $this->proxy->getOCSData($proxy);
$this->participantService->updateUnreadInfoForProxyParticipant(
$participant,
$data['unreadMessages'],
$data['unreadMention'],
$data['unreadMentionDirect'],
$data['lastReadMessage'],
);
$headers = $lastCommonRead = [];
if ($proxy->getHeader('X-Chat-Last-Common-Read')) {
$lastCommonRead[$room->getId()] = (int)$proxy->getHeader('X-Chat-Last-Common-Read');
$headers['X-Chat-Last-Common-Read'] = (string)$lastCommonRead[$room->getId()];
}
return new DataResponse($this->roomFormatter->formatRoom(
$responseFormat,
$lastCommonRead,
$room,
$participant,
), Http::STATUS_OK, $headers);
}
/**
* @see \OCA\Talk\Controller\ChatController::mentions()
*
* @return DataResponse<Http::STATUS_OK, list<TalkChatMentionSuggestion>, array{}>
* @throws CannotReachRemoteException
*
* 200: List of mention suggestions returned
*/
public function mentions(Room $room, Participant $participant, string $search, int $limit, bool $includeStatus): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/mentions',
[
'search' => $search,
'limit' => $limit,
'includeStatus' => $includeStatus,
],
);
/** @var list<TalkChatMentionSuggestion> $data */
$data = $this->proxy->getOCSData($proxy);
/** @var list<TalkChatMentionSuggestion> $data */
$data = $this->userConverter->convertAttendees($room, $data, 'source', 'id', 'label');
// FIXME post-load status information
return new DataResponse($data, Http::STATUS_OK);
}
}
@@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\PollPropertyException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use Psr\Log\LoggerInterface;
/**
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
*/
class PollController {
public function __construct(
protected ProxyRequest $proxy,
protected UserConverter $userConverter,
protected LoggerInterface $logger,
) {
}
/**
* @return DataResponse<Http::STATUS_OK, list<TalkPollDraft>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
* @throws CannotReachRemoteException
*
* 200: Polls returned
* 404: Polls not found
*
* @see \OCA\Talk\Controller\PollController::showPoll()
*/
public function getDraftsForRoom(Room $room, Participant $participant): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/drafts',
);
$status = $proxy->getStatusCode();
if ($status === Http::STATUS_NOT_FOUND || $status === Http::STATUS_FORBIDDEN) {
return new DataResponse([], $status);
}
/** @var list<TalkPollDraft> $list */
$list = $this->proxy->getOCSData($proxy);
$data = [];
foreach ($list as $poll) {
$data[] = $this->userConverter->convertPoll($room, $poll);
}
return new DataResponse($data);
}
/**
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Poll returned
* 404: Poll not found
*
* @see \OCA\Talk\Controller\PollController::showPoll()
*/
public function showPoll(Room $room, Participant $participant, int $pollId): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId,
);
if ($proxy->getStatusCode() === Http::STATUS_NOT_FOUND) {
/** @var array{error?: string} $data */
$data = $this->proxy->getOCSData($proxy);
return new DataResponse(['error' => $data['error'] ?? 'poll'], Http::STATUS_NOT_FOUND);
}
/** @var TalkPoll $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertPoll($room, $data);
return new DataResponse($data);
}
/**
* @param list<int> $optionIds
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @throws CannotReachRemoteException
*
* 200: Voted successfully
* 400: Voting is not possible
* 404: Poll not found
*
* @see \OCA\Talk\Controller\PollController::votePoll()
*/
public function votePoll(Room $room, Participant $participant, int $pollId, array $optionIds): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId,
['optionIds' => $optionIds],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_NOT_FOUND,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
/** @var array{error?: string} $data */
$data = $this->proxy->getOCSData($proxy);
return new DataResponse(['error' => $data['error'] ?? 'poll'], $statusCode);
}
/** @var TalkPoll $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertPoll($room, $data);
return new DataResponse($data);
}
/**
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'poll'|'question'|'room'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Draft created successfully
* 201: Poll created successfully
* 400: Creating poll is not possible
*
* @see \OCA\Talk\Controller\PollController::createPoll()
*/
public function createPoll(Room $room, Participant $participant, string $question, array $options, int $resultMode, int $maxVotes, bool $draft): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken(),
[
'question' => $question,
'options' => $options,
'resultMode' => $resultMode,
'maxVotes' => $maxVotes,
'draft' => $draft,
],
);
$status = $proxy->getStatusCode();
if ($status === Http::STATUS_BAD_REQUEST) {
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]);
return new DataResponse($data, Http::STATUS_BAD_REQUEST);
}
/** @var TalkPoll $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_CREATED]);
$data = $this->userConverter->convertPoll($room, $data);
if ($status === Http::STATUS_OK) {
return new DataResponse($data);
}
return new DataResponse($data, Http::STATUS_CREATED);
}
/**
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'draft'|'options'|'poll'|'question'|'room'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Draft created successfully
* 201: Poll created successfully
* 400: Creating poll is not possible
*
* @see \OCA\Talk\Controller\PollController::createPoll()
*/
public function updateDraftPoll(int $pollId, Room $room, Participant $participant, string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/draft/' . $pollId,
[
'question' => $question,
'options' => $options,
'resultMode' => $resultMode,
'maxVotes' => $maxVotes
],
);
$status = $proxy->getStatusCode();
if ($status === Http::STATUS_BAD_REQUEST) {
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]);
return new DataResponse($data, Http::STATUS_BAD_REQUEST);
}
/** @var TalkPollDraft $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_CREATED]);
$data = $this->userConverter->convertPoll($room, $data);
if ($status === Http::STATUS_OK) {
return new DataResponse($data);
}
return new DataResponse($data);
}
/**
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'poll'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Poll closed successfully
* 400: Poll already closed
* 403: Missing permissions to close poll
* 404: Poll not found
*
* @see \OCA\Talk\Controller\PollController::closePoll()
*/
public function closePoll(Room $room, Participant $participant, int $pollId): DataResponse {
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId,
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_FORBIDDEN,
Http::STATUS_NOT_FOUND,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
/** @var array{error?: string} $data */
$data = $this->proxy->getOCSData($proxy);
if ($data['error'] !== PollPropertyException::REASON_POLL) {
$this->logger->error('Unhandled error in ' . __METHOD__ . ': ' . $data['error']);
}
return new DataResponse(['error' => PollPropertyException::REASON_POLL], $statusCode);
}
/** @var TalkPoll $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertPoll($room, $data);
return new DataResponse($data);
}
}
@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
/**
* @psalm-import-type TalkReaction from ResponseDefinitions
*/
class ReactionController {
public function __construct(
protected ProxyRequest $proxy,
protected UserConverter $userConverter,
) {
}
/**
* Add a reaction to a message
*
* @param int $messageId ID of the message
* @psalm-param non-negative-int $messageId
* @param string $reaction Emoji to add
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, array<string, list<TalkReaction>>|\stdClass, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Reaction already existed
* 201: Reaction added successfully
* 400: Adding reaction is not possible
* 404: Message not found
*
* @see \OCA\Talk\Controller\ReactionController::react()
*/
public function react(Room $room, Participant $participant, int $messageId, string $reaction, string $format): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/reaction/' . $room->getRemoteToken() . '/' . $messageId,
[
'reaction' => $reaction,
],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK && $statusCode !== Http::STATUS_CREATED) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_NOT_FOUND,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
return new DataResponse(null, $statusCode);
}
/** @var array<string, list<TalkReaction>> $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_CREATED, Http::STATUS_OK]);
$data = $this->userConverter->convertReactionsList($room, $data);
return new DataResponse($this->formatReactions($format, $data), $statusCode);
}
/**
* Delete a reaction from a message
*
* @param int $messageId ID of the message
* @psalm-param non-negative-int $messageId
* @param string $reaction Emoji to remove
* @return DataResponse<Http::STATUS_OK, array<string, list<TalkReaction>>|\stdClass, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Reaction deleted successfully
* 400: Deleting reaction is not possible
* 404: Message not found
*
* @see \OCA\Talk\Controller\ReactionController::delete()
*/
public function delete(Room $room, Participant $participant, int $messageId, string $reaction, string $format): DataResponse {
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/reaction/' . $room->getRemoteToken() . '/' . $messageId,
[
'reaction' => $reaction,
],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_NOT_FOUND,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
return new DataResponse(null, $statusCode);
}
/** @var array<string, list<TalkReaction>> $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertReactionsList($room, $data);
return new DataResponse($this->formatReactions($format, $data), $statusCode);
}
/**
* Get a list of reactions for a message
*
* @param int $messageId ID of the message
* @psalm-param non-negative-int $messageId
* @param string|null $reaction Emoji to filter
* @return DataResponse<Http::STATUS_OK, array<string, list<TalkReaction>>|\stdClass, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Reactions returned
* 404: Message or reaction not found
*
* @see \OCA\Talk\Controller\ReactionController::getReactions()
*/
public function getReactions(Room $room, Participant $participant, int $messageId, ?string $reaction, string $format): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/reaction/' . $room->getRemoteToken() . '/' . $messageId,
$reaction === null ? [] : [
'reaction' => $reaction,
],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK) {
if ($statusCode !== Http::STATUS_NOT_FOUND) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
}
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
/** @var array<string, list<TalkReaction>> $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertReactionsList($room, $data);
return new DataResponse($this->formatReactions($format, $data), $statusCode);
}
/**
* @param array<string, list<TalkReaction>> $reactions
* @return array<string, list<TalkReaction>>|\stdClass
*/
protected function formatReactions(string $format, array $reactions): array|\stdClass {
if ($format === 'json' && empty($reactions)) {
// Cheating here to make sure the reactions array is always a
// JSON object on the API, even when there is no reaction at all.
return new \stdClass();
}
return $reactions;
}
}
@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Model\Session;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
/**
* @psalm-import-type TalkCapabilities from ResponseDefinitions
* @psalm-import-type TalkParticipant from ResponseDefinitions
* @psalm-import-type TalkRoom from ResponseDefinitions
*/
class RoomController {
public function __construct(
protected ProxyRequest $proxy,
protected UserConverter $userConverter,
) {
}
/**
* @see \OCA\Talk\Controller\RoomController::getParticipants()
*
* @return DataResponse<Http::STATUS_OK, list<TalkParticipant>, array{X-Nextcloud-Has-User-Statuses?: bool}>
* @throws CannotReachRemoteException
*
* 200: Participants returned
* 403: Missing permissions for getting participants
*/
public function getParticipants(Room $room, Participant $participant): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/participants',
);
/** @var list<TalkParticipant> $data */
$data = $this->proxy->getOCSData($proxy);
/** @var list<TalkParticipant> $data */
$data = $this->userConverter->convertAttendees($room, $data, 'actorType', 'actorId', 'displayName');
$headers = [];
if ($proxy->getHeader('X-Nextcloud-Has-User-Statuses')) {
$headers['X-Nextcloud-Has-User-Statuses'] = (bool)$proxy->getHeader('X-Nextcloud-Has-User-Statuses');
}
return new DataResponse($data, Http::STATUS_OK, $headers);
}
/**
* @see \OCA\Talk\Controller\RoomController::joinFederatedRoom()
*
* @param Room $room the federated room to join
* @param Participant $participant the federated user to will join the room;
* the participant must have a session
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{X-Nextcloud-Talk-Proxy-Hash: string}>
* @throws CannotReachRemoteException
*
* 200: Federated user joined the room
* 404: Room not found
*/
public function joinFederatedRoom(Room $room, Participant $participant): DataResponse {
$options = [
'sessionId' => $participant->getSession()->getSessionId(),
];
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/federation/active',
$options,
);
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
$headers = ['X-Nextcloud-Talk-Proxy-Hash' => $this->proxy->overwrittenRemoteTalkHash($proxy->getHeader('X-Nextcloud-Talk-Hash'))];
/** @var TalkRoom[] $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertAttendee($room, $data, 'actorType', 'actorId', 'displayName');
return new DataResponse($data, $statusCode, $headers);
}
/**
* @see \OCA\Talk\Controller\RoomController::leaveFederatedRoom()
*
* @param Room $room the federated room to leave
* @param Participant $participant the federated user that will leave the
* room; the participant must have a session
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
* @throws CannotReachRemoteException
*
* 200: Federated user left the room
*/
public function leaveFederatedRoom(Room $room, Participant $participant): DataResponse {
$options = [
'sessionId' => $participant->getSession()->getSessionId(),
];
$proxy = $this->proxy->delete(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/federation/active',
$options,
);
// STATUS_NOT_FOUND is not taken into account, as it should happen only
// for non-federation requests.
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
return new DataResponse([], $statusCode);
}
/**
* @see \OCA\Talk\Controller\RoomController::getCapabilities()
*
* @return DataResponse<Http::STATUS_OK, TalkCapabilities|array<empty>, array{X-Nextcloud-Talk-Hash: string}>
* @throws CannotReachRemoteException
*
* 200: Get capabilities successfully
*/
public function getCapabilities(Room $room, Participant $participant): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/capabilities',
);
/** @var TalkCapabilities|array<empty> $data */
$data = $this->proxy->getOCSData($proxy);
$headers = [
'X-Nextcloud-Talk-Hash' => $this->proxy->overwrittenRemoteTalkHash($proxy->getHeader('X-Nextcloud-Talk-Hash')),
];
return new DataResponse($data, Http::STATUS_OK, $headers);
}
/**
* @return array<string, mixed>|\stdClass
*/
protected function emptyArray(): array|\stdClass {
// Cheating here to make sure the array is always a
// JSON object on the API, even when there is no entry at all.
return new \stdClass();
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
/**
* @psalm-import-type TalkSignalingSettings from ResponseDefinitions
*/
class SignalingController {
public function __construct(
protected ProxyRequest $proxy,
) {
}
/**
* @see \OCA\Talk\Controller\SignalingController::getSettings()
*
* @return DataResponse<Http::STATUS_OK, TalkSignalingSettings, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @throws CannotReachRemoteException
*
* 200: Signaling settings returned
* 404: Room not found
*/
public function getSettings(Room $room, Participant $participant): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v3/signaling/settings',
[
'token' => $room->getRemoteToken(),
],
);
$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}
/** @var TalkSignalingSettings|array<empty> $data */
$data = $this->proxy->getOCSData($proxy);
return new DataResponse($data, $statusCode);
}
}
@@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Chat\Notifier;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Participant;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RoomFormatter;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\ICacheFactory;
/**
* @psalm-import-type TalkThreadInfo from ResponseDefinitions
* @psalm-import-type TalkRoom from ResponseDefinitions
*/
class ThreadController {
public function __construct(
protected ProxyRequest $proxy,
protected UserConverter $userConverter,
protected ParticipantService $participantService,
protected RoomFormatter $roomFormatter,
protected Notifier $notifier,
ICacheFactory $cacheFactory,
) {
}
/**
* @see \OCA\Talk\Controller\ThreadController::getRecentActiveThreads()
*
* @return DataResponse<Http::STATUS_OK, list<TalkThreadInfo>, array{}>
* @throws CannotReachRemoteException
*
* 200: List of threads returned
*/
public function getRecentActiveThreads(Room $room, Participant $participant, int $limit): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/threads/recent',
[
'limit' => $limit,
],
);
/** @var list<TalkThreadInfo> $data */
$data = $this->proxy->getOCSData($proxy);
if (!empty($data)) {
$data = $this->userConverter->convertThreadInfos($room, $data);
}
return new DataResponse($data);
}
/**
* @see \OCA\Talk\Controller\ThreadController::getThread()
*
* @psalm-param non-negative-int $threadId
* @return DataResponse<Http::STATUS_OK, TalkThreadInfo, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'thread'|'status'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Thread info returned
* 404: Thread not found
*/
public function getThread(Room $room, Participant $participant, int $threadId): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/threads/' . $threadId,
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK) {
if ($statusCode !== Http::STATUS_NOT_FOUND) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
$data = ['error' => 'status'];
} else {
/** @var array{error: 'thread'} $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_NOT_FOUND]);
}
return new DataResponse($data, Http::STATUS_NOT_FOUND);
}
/** @var TalkThreadInfo $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertThreadInfo($room, $data);
return new DataResponse($data, Http::STATUS_OK);
}
/**
* @see \OCA\Talk\Controller\ThreadController::setNotificationLevel()
*
* @psalm-param non-negative-int $threadId
* @return DataResponse<Http::STATUS_OK, TalkThreadInfo, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'title'}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'thread'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Thread renamed successfully
* 400: When the provided title is empty
* 404: Thread not found
*/
public function renameThread(Room $room, Participant $participant, int $threadId, string $threadTitle): DataResponse {
$proxy = $this->proxy->put(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/threads/' . $threadId,
['threadTitle' => $threadTitle],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_NOT_FOUND,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
$data = ['error' => 'thread'];
} elseif ($statusCode === Http::STATUS_BAD_REQUEST) {
/** @var array{error: 'title'} $data */
$data = $this->proxy->getOCSData($proxy, [
Http::STATUS_BAD_REQUEST,
]);
} else {
/** @var array{error: 'thread'} $data */
$data = $this->proxy->getOCSData($proxy, [
Http::STATUS_NOT_FOUND,
]);
}
return new DataResponse($data, $statusCode);
}
/** @var TalkThreadInfo $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertThreadInfo($room, $data);
return new DataResponse($data, Http::STATUS_OK);
}
/**
* @see \OCA\Talk\Controller\ThreadController::setNotificationLevel()
*
* @psalm-param non-negative-int $messageId
* @psalm-param Participant::NOTIFY_* $level
* @return DataResponse<Http::STATUS_OK, TalkThreadInfo, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{error: 'level'|'message'|'status'|'top-most'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Successfully set notification level for thread
* 400: Notification level was invalid
* 404: Message or top most message not found
*/
public function setNotificationLevel(Room $room, Participant $participant, int $messageId, int $level): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/threads/' . $messageId . '/notify',
['level' => $level],
);
$statusCode = $proxy->getStatusCode();
if ($statusCode !== Http::STATUS_OK) {
if (!in_array($statusCode, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_NOT_FOUND,
], true)) {
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
$data = ['error' => 'status'];
} else {
/** @var array{error: 'level'|'message'|'top-most'} $data */
$data = $this->proxy->getOCSData($proxy, [
Http::STATUS_BAD_REQUEST,
Http::STATUS_NOT_FOUND,
]);
}
return new DataResponse($data, $statusCode);
}
/** @var TalkThreadInfo $data */
$data = $this->proxy->getOCSData($proxy);
$data = $this->userConverter->convertThreadInfo($room, $data);
return new DataResponse($data, Http::STATUS_OK);
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Listener;
use OCA\Talk\Config;
use OCA\Talk\Federation\CloudFederationProviderTalk;
use OCA\Talk\Federation\FederationManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\OCM\Events\ResourceTypeRegisterEvent;
use OCP\OCM\IOCMProvider;
/**
* @template-implements IEventListener<Event>
*/
class ResourceTypeRegisterListener implements IEventListener {
public function __construct(
protected Config $talkConfig,
protected IOCMProvider $provider,
protected CloudFederationProviderTalk $talkProvider,
) {
}
#[\Override]
public function handle(Event $event): void {
if (!$event instanceof ResourceTypeRegisterEvent) {
// Unrelated
return;
}
if (!$this->talkConfig->isFederationEnabled()) {
return;
}
$event->registerResourceType(
FederationManager::TALK_ROOM_RESOURCE,
$this->talkProvider->getSupportedShareTypes(),
[
'talk-v1' => '/ocs/v2.php/apps/spreed/api/',
]
);
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier;
use OCA\Talk\Events\BeforeRoomDeletedEvent;
use OCA\Talk\Federation\BackendNotifier;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\ParticipantService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Federation\ICloudIdManager;
/**
* @template-implements IEventListener<Event>
*/
class BeforeRoomDeletedListener implements IEventListener {
public function __construct(
protected BackendNotifier $backendNotifier,
protected ParticipantService $participantService,
protected ICloudIdManager $cloudIdManager,
) {
}
#[\Override]
public function handle(Event $event): void {
if (!$event instanceof BeforeRoomDeletedEvent) {
return;
}
$participants = $this->participantService->getParticipantsByActorType($event->getRoom(), Attendee::ACTOR_FEDERATED_USERS);
foreach ($participants as $participant) {
$cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId());
$this->backendNotifier->sendRemoteUnShare(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
);
}
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier;
use OCA\Talk\Events\AttendeeRemovedEvent;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\RetryNotificationMapper;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
/**
* @template-implements IEventListener<Event>
*/
class CancelRetryOCMListener implements IEventListener {
public function __construct(
protected RetryNotificationMapper $retryNotificationMapper,
) {
}
#[\Override]
public function handle(Event $event): void {
if (!$event instanceof AttendeeRemovedEvent) {
return;
}
$attendee = $event->getAttendee();
if ($attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) {
return;
}
$this->retryNotificationMapper->deleteByProviderId(
(string)$event->getAttendee()->getId()
);
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\ASystemMessageSentEvent;
use OCA\Talk\Events\ChatMessageSentEvent;
use OCA\Talk\Events\SystemMessageSentEvent;
use OCA\Talk\Events\SystemMessagesMultipleSentEvent;
use OCA\Talk\Federation\BackendNotifier;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\ProxyCacheMessage;
use OCA\Talk\Service\ParticipantService;
use OCP\Comments\IComment;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Federation\ICloudIdManager;
use OCP\L10N\IFactory;
/**
* @template-implements IEventListener<Event>
*/
class MessageSentListener implements IEventListener {
public function __construct(
protected BackendNotifier $backendNotifier,
protected ParticipantService $participantService,
protected ICloudIdManager $cloudIdManager,
protected MessageParser $messageParser,
protected IFactory $l10nFactory,
protected ChatManager $chatManager,
) {
}
#[\Override]
public function handle(Event $event): void {
if (!$event instanceof ChatMessageSentEvent
&& !$event instanceof SystemMessageSentEvent
&& !$event instanceof SystemMessagesMultipleSentEvent) {
return;
}
// FIXME once we store/cache the info skip this if the room has no federation participant
// if (!$event->getRoom()->hasFederatedParticipants()) {
// return;
// }
// Try to have as neutral as possible messages
$l = $this->l10nFactory->get('spreed', 'en', 'en');
$chatMessage = $this->messageParser->createMessage($event->getRoom(), null, $event->getComment(), $l);
$this->messageParser->parseMessage($chatMessage);
$systemMessage = $chatMessage->getMessageType() === ChatManager::VERB_SYSTEM ? $chatMessage->getMessageRaw() : '';
if ($systemMessage !== 'message_edited'
&& $systemMessage !== 'message_deleted'
&& $event instanceof ASystemMessageSentEvent
&& $event->shouldSkipLastActivityUpdate()) {
return;
}
if (!$chatMessage->getVisibility()) {
return;
}
$expireDate = $event->getComment()->getExpireDate();
$creationDate = $event->getComment()->getCreationDateTime();
$metaData = $event->getComment()->getMetaData() ?? [];
$parent = $event->getParent();
if ($parent instanceof IComment) {
$metaData[ProxyCacheMessage::METADATA_REPLY_TO_ACTOR_TYPE] = $parent->getActorType();
$metaData[ProxyCacheMessage::METADATA_REPLY_TO_ACTOR_ID] = $parent->getActorId();
$metaData[ProxyCacheMessage::METADATA_REPLY_TO_MESSAGE_ID] = (int)$parent->getId();
}
$messageData = [
'remoteMessageId' => (int)$event->getComment()->getId(),
'actorType' => $chatMessage->getActorType(),
'actorId' => $chatMessage->getActorId(),
'actorDisplayName' => $chatMessage->getActorDisplayName(),
'messageType' => $chatMessage->getMessageType(),
'systemMessage' => $systemMessage,
'expirationDatetime' => $expireDate ? $expireDate->format(\DateTime::ATOM) : '',
'message' => $chatMessage->getMessage(),
'messageParameter' => json_encode($chatMessage->getMessageParameters(), JSON_THROW_ON_ERROR),
'creationDatetime' => $creationDate->format(\DateTime::ATOM),
'metaData' => json_encode($metaData, JSON_THROW_ON_ERROR),
];
$participants = $this->participantService->getParticipantsByActorType($event->getRoom(), Attendee::ACTOR_FEDERATED_USERS);
foreach ($participants as $participant) {
$attendee = $participant->getAttendee();
$cloudId = $this->cloudIdManager->resolveCloudId($attendee->getActorId());
$lastReadMessage = $attendee->getLastReadMessage();
$lastMention = $attendee->getLastMentionMessage();
$lastMentionDirect = $attendee->getLastMentionDirect();
$unreadInfo = [
'lastReadMessage' => $lastReadMessage,
'unreadMessages' => $this->chatManager->getUnreadCount($event->getRoom(), $lastReadMessage),
'unreadMention' => $lastMention !== 0 && $lastReadMessage < $lastMention,
'unreadMentionDirect' => $lastMentionDirect !== 0 && $lastReadMessage < $lastMentionDirect,
];
$success = $this->backendNotifier->sendMessageUpdate(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
$event->getRoom()->getToken(),
$messageData,
$unreadInfo,
);
if ($success === null) {
$this->participantService->removeAttendee($event->getRoom(), $participant, AAttendeeRemovedEvent::REASON_LEFT);
}
}
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier;
use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\AParticipantModifiedEvent;
use OCA\Talk\Events\ParticipantModifiedEvent;
use OCA\Talk\Federation\BackendNotifier;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Participant;
use OCA\Talk\Service\ParticipantService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;
/**
* @template-implements IEventListener<Event>
*/
class ParticipantModifiedListener implements IEventListener {
public function __construct(
protected BackendNotifier $backendNotifier,
protected ParticipantService $participantService,
protected ICloudIdManager $cloudIdManager,
) {
}
#[\Override]
public function handle(Event $event): void {
if (!$event instanceof ParticipantModifiedEvent) {
return;
}
$participant = $event->getParticipant();
if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) {
return;
}
if (!in_array($event->getProperty(), [
AParticipantModifiedEvent::PROPERTY_PERMISSIONS,
AParticipantModifiedEvent::PROPERTY_RESEND_CALL,
], true)) {
return;
}
// For modifying participants we only notify the affected participant's server
$cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId());
$success = $this->notifyParticipantModified($cloudId, $participant, $event);
if ($success === null) {
$this->participantService->removeAttendee($event->getRoom(), $participant, AAttendeeRemovedEvent::REASON_LEFT);
}
}
private function notifyParticipantModified(ICloudId $cloudId, Participant $participant, AParticipantModifiedEvent $event): ?bool {
return $this->backendNotifier->sendParticipantModifiedUpdate(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
$event->getRoom()->getToken(),
$event->getProperty(),
$event->getNewValue(),
$event->getOldValue(),
);
}
}
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier;
use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\ALobbyModifiedEvent;
use OCA\Talk\Events\AParticipantModifiedEvent;
use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\CallEndedEvent;
use OCA\Talk\Events\CallEndedForEveryoneEvent;
use OCA\Talk\Events\CallStartedEvent;
use OCA\Talk\Events\LobbyModifiedEvent;
use OCA\Talk\Events\RoomModifiedEvent;
use OCA\Talk\Federation\BackendNotifier;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Participant;
use OCA\Talk\Service\ParticipantService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;
/**
* @template-implements IEventListener<Event>
*/
class RoomModifiedListener implements IEventListener {
public function __construct(
protected BackendNotifier $backendNotifier,
protected ParticipantService $participantService,
protected ICloudIdManager $cloudIdManager,
) {
}
#[\Override]
public function handle(Event $event): void {
if (!$event instanceof CallStartedEvent
&& !$event instanceof CallEndedEvent
&& !$event instanceof CallEndedForEveryoneEvent
&& !$event instanceof LobbyModifiedEvent
&& !$event instanceof RoomModifiedEvent) {
return;
}
if (!in_array($event->getProperty(), [
ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE,
ARoomModifiedEvent::PROPERTY_AVATAR,
ARoomModifiedEvent::PROPERTY_CALL_RECORDING,
ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS,
ARoomModifiedEvent::PROPERTY_DESCRIPTION,
ARoomModifiedEvent::PROPERTY_IN_CALL,
ARoomModifiedEvent::PROPERTY_LOBBY,
ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS,
ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION,
ARoomModifiedEvent::PROPERTY_NAME,
ARoomModifiedEvent::PROPERTY_READ_ONLY,
ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT,
ARoomModifiedEvent::PROPERTY_SIP_ENABLED,
ARoomModifiedEvent::PROPERTY_TYPE,
], true)) {
return;
}
if ($event->getRoom()->isFederatedConversation()) {
return;
}
$participants = $this->participantService->getParticipantsByActorType($event->getRoom(), Attendee::ACTOR_FEDERATED_USERS);
foreach ($participants as $participant) {
$cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId());
if ($event instanceof CallStartedEvent) {
$success = $this->notifyCallStarted($cloudId, $participant, $event);
} elseif ($event instanceof CallEndedEvent || $event instanceof CallEndedForEveryoneEvent) {
$success = $this->notifyCallEnded($cloudId, $participant, $event);
} elseif ($event instanceof ALobbyModifiedEvent) {
$success = $this->notifyLobbyModified($cloudId, $participant, $event);
} else {
$success = $this->notifyRoomModified($cloudId, $participant, $event);
}
if ($success === null) {
$this->participantService->removeAttendee($event->getRoom(), $participant, AAttendeeRemovedEvent::REASON_LEFT);
}
}
}
private function notifyCallStarted(ICloudId $cloudId, Participant $participant, CallStartedEvent $event) {
$details = [];
if ($event->getDetail(AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT)) {
$details = [AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT => true];
}
return $this->backendNotifier->sendCallStarted(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
$event->getRoom()->getToken(),
$event->getProperty(),
$event->getNewValue(),
$event->getCallFlag(),
$details,
);
}
private function notifyCallEnded(ICloudId $cloudId, Participant $participant, CallEndedEvent|CallEndedForEveryoneEvent $event) {
$details = [];
if ($event instanceof CallEndedForEveryoneEvent) {
$details = [AParticipantModifiedEvent::DETAIL_IN_CALL_END_FOR_EVERYONE => true];
}
return $this->backendNotifier->sendCallEnded(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
$event->getRoom()->getToken(),
ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE,
null,
Participant::FLAG_DISCONNECTED,
$details,
);
}
private function notifyLobbyModified(ICloudId $cloudId, Participant $participant, ALobbyModifiedEvent $event) {
return $this->backendNotifier->sendRoomModifiedLobbyUpdate(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
$event->getRoom()->getToken(),
$event->getProperty(),
$event->getNewValue(),
$event->getOldValue(),
$event->getLobbyTimer(),
$event->isTimerReached(),
);
}
private function notifyRoomModified(ICloudId $cloudId, Participant $participant, ARoomModifiedEvent $event) {
return $this->backendNotifier->sendRoomModifiedUpdate(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
$event->getRoom()->getToken(),
$event->getProperty(),
$event->getNewValue(),
$event->getOldValue(),
);
}
}
@@ -0,0 +1,260 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use OC\Http\Client\Response;
use OCA\Talk\AppInfo\Application;
use OCA\Talk\Config as TalkConfig;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\RemoteClientException;
use OCA\Talk\Participant;
use OCA\Talk\Settings\UserPreference;
use OCP\AppFramework\Http;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\IConfig;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use Psr\Log\LoggerInterface;
use SensitiveParameter;
class ProxyRequest {
public function __construct(
protected IConfig $config,
protected IClientService $clientService,
protected LoggerInterface $logger,
protected IFactory $l10nFactory,
protected IUserSession $userSession,
protected TalkConfig $talkConfig,
) {
}
public function overwrittenRemoteTalkHash(string $hash): string {
$typingIndicator = $this->config->getUserValue(
$this->userSession->getUser()?->getUID(),
Application::APP_ID,
UserPreference::TYPING_PRIVACY,
Participant::PRIVACY_PRIVATE,
);
return sha1(json_encode([
'remoteHash' => $hash,
'manipulated' => [
'config' => [
'chat' => [
'read-privacy',
'typing-privacy' => $typingIndicator,
],
'call' => [
'blur-virtual-background',
],
'conversations' => [
'list-style',
],
],
]
]));
}
/**
* @return Http::STATUS_BAD_REQUEST
*/
public function logUnexpectedStatusCode(string $method, int $statusCode, string $logDetails = ''): int {
if ($this->config->getSystemValueBool('debug')) {
$this->logger->error('Unexpected status code ' . $statusCode . ' returned for ' . $method . ($logDetails !== '' ? "\n" . $logDetails : ''));
} else {
$this->logger->debug('Unexpected status code ' . $statusCode . ' returned for ' . $method . ($logDetails !== '' ? "\n" . $logDetails : ''));
}
return Http::STATUS_BAD_REQUEST;
}
protected function generateDefaultRequestOptions(
?string $cloudId,
#[SensitiveParameter]
?string $accessToken,
): array {
$options = [
'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates'),
'nextcloud' => [
'allow_local_address' => $this->config->getSystemValueBool('allow_local_remote_servers'),
],
'headers' => [
'Accept' => 'application/json',
'X-Nextcloud-Federation' => 'true',
'OCS-APIRequest' => 'true',
'Accept-Language' => $this->l10nFactory->getUserLanguage($this->userSession->getUser()),
],
'timeout' => 5,
];
if ($cloudId !== null && $accessToken !== null) {
$options['auth'] = [urlencode($cloudId), $accessToken];
}
return $options;
}
protected function prependProtocolIfNotAvailable(string $url): string {
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
$url = 'https://' . $url;
}
return $url;
}
/**
* @param 'get'|'post'|'put'|'delete' $verb
* @throws CannotReachRemoteException
*/
protected function request(
string $verb,
?string $cloudId,
#[SensitiveParameter]
?string $accessToken,
string $url,
array $parameters,
): IResponse {
if (!$this->talkConfig->isFederationEnabled()) {
throw new CannotReachRemoteException();
}
$requestOptions = $this->generateDefaultRequestOptions($cloudId, $accessToken);
if (!empty($parameters)) {
$requestOptions['json'] = $parameters;
}
try {
return $this->clientService->newClient()->{$verb}(
$this->prependProtocolIfNotAvailable($url),
$requestOptions
);
} catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode();
try {
$body = $e->getResponse()->getBody()->getContents();
$data = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
$e->getResponse()->getBody()->rewind();
if (!is_array($data)) {
throw new \RuntimeException('JSON response is not an array');
}
} catch (\Throwable $e) {
throw new CannotReachRemoteException('Error parsing JSON response', $e->getCode(), $e);
}
$clientException = new RemoteClientException($e->getMessage(), $status, $e, $data);
$this->logger->debug('Client error from remote', ['exception' => $clientException]);
return new Response($e->getResponse(), false);
} catch (ServerException|\Throwable $e) {
$serverException = new CannotReachRemoteException($e->getMessage(), $e->getCode(), $e);
$this->logger->error('Could not reach remote', ['exception' => $serverException]);
throw $serverException;
}
}
/**
* @throws CannotReachRemoteException
*/
public function get(
?string $cloudId,
#[SensitiveParameter]
?string $accessToken,
string $url,
array $parameters = [],
): IResponse {
return $this->request(
'get',
$cloudId,
$accessToken,
$url,
$parameters,
);
}
/**
* @throws CannotReachRemoteException
*/
public function put(
string $cloudId,
#[SensitiveParameter]
string $accessToken,
string $url,
array $parameters = [],
): IResponse {
return $this->request(
'put',
$cloudId,
$accessToken,
$url,
$parameters,
);
}
/**
* @throws CannotReachRemoteException
*/
public function delete(
string $cloudId,
#[SensitiveParameter]
string $accessToken,
string $url,
array $parameters = [],
): IResponse {
return $this->request(
'delete',
$cloudId,
$accessToken,
$url,
$parameters,
);
}
/**
* @throws CannotReachRemoteException
*/
public function post(
string $cloudId,
#[SensitiveParameter]
string $accessToken,
string $url,
array $parameters = [],
): IResponse {
return $this->request(
'post',
$cloudId,
$accessToken,
$url,
$parameters,
);
}
/**
* @param list<int> $allowedStatusCodes
* @throws CannotReachRemoteException
*/
public function getOCSData(IResponse $response, array $allowedStatusCodes = [Http::STATUS_OK]): array {
if (!in_array($response->getStatusCode(), $allowedStatusCodes, true)) {
$this->logUnexpectedStatusCode(__METHOD__, $response->getStatusCode());
}
try {
$content = $response->getBody();
$responseData = json_decode($content, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($responseData)) {
throw new \RuntimeException('JSON response is not an array');
}
} catch (\Throwable $e) {
$this->logger->error('Error parsing JSON response: ' . ($content ?? 'no-data'), ['exception' => $e]);
throw new CannotReachRemoteException('Error parsing JSON response', $e->getCode(), $e);
}
return $responseData['ocs']['data'] ?? [];
}
}
@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Federation\Proxy\TalkV1;
use OCA\Talk\Model\Attendee;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\ParticipantService;
/**
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
* @psalm-import-type TalkReaction from ResponseDefinitions
* @psalm-import-type TalkThreadInfo from ResponseDefinitions
*/
class UserConverter {
/**
* @var array<string, array<string, array{userId: string, displayName: string}>>
*/
protected array $participantsPerRoom = [];
public function __construct(
protected ParticipantService $participantService,
protected AvatarService $avatarService,
) {
}
/**
* @return array{type: string, id: string}
*/
public function convertTypeAndId(Room $room, string $type, string $id): array {
if ($type === Attendee::ACTOR_USERS) {
$type = Attendee::ACTOR_FEDERATED_USERS;
$id = $this->createCloudIdFromUserIdAndFullServerUrl($id, $room->getRemoteServer());
} elseif ($type === Attendee::ACTOR_FEDERATED_USERS) {
$localParticipants = $this->getLocalParticipants($room);
if (isset($localParticipants[$id])) {
$local = $localParticipants[$id];
$type = Attendee::ACTOR_USERS;
$id = $local['userId'];
}
}
return ['type' => $type, 'id' => $id];
}
public function convertAttendee(Room $room, array $entry, string $typeField, string $idField, string $displayNameField): array {
if (!isset($entry[$typeField])) {
return $entry;
}
if ($entry[$typeField] === Attendee::ACTOR_USERS) {
$entry[$typeField] = Attendee::ACTOR_FEDERATED_USERS;
$entry[$idField] = $this->createCloudIdFromUserIdAndFullServerUrl($entry[$idField], $room->getRemoteServer());
} elseif ($entry[$typeField] === Attendee::ACTOR_FEDERATED_USERS) {
$localParticipants = $this->getLocalParticipants($room);
if (isset($localParticipants[$entry[$idField]])) {
$local = $localParticipants[$entry[$idField]];
$entry[$typeField] = Attendee::ACTOR_USERS;
$entry[$idField] = $local['userId'];
$entry[$displayNameField] = $local['displayName'];
}
}
return $entry;
}
public function convertAttendees(Room $room, array $entries, string $typeField, string $idField, string $displayNameField): array {
return array_map(
fn (array $entry): array => $this->convertAttendee($room, $entry, $typeField, $idField, $displayNameField),
$entries
);
}
protected function convertMessageParameter(Room $room, array $parameter): array {
if ($parameter['type'] === 'user') { // RichObjectDefinition, not Attendee::ACTOR_USERS
if (!isset($parameter['server'])) {
$parameter['server'] = $room->getRemoteServer();
if (!isset($parameter['mention-id'])) {
$parameter['mention-id'] = $parameter['id'];
}
} elseif ($parameter['server']) {
$localParticipants = $this->getLocalParticipants($room);
$cloudId = $this->createCloudIdFromUserIdAndFullServerUrl($parameter['id'], $parameter['server']);
if (!isset($parameter['mention-id'])) {
$parameter['mention-id'] = 'federated_user/' . $parameter['id'] . '@' . $parameter['server'];
}
if (isset($localParticipants[$cloudId])) {
unset($parameter['server']);
$parameter['name'] = $localParticipants[$cloudId]['displayName'];
}
}
} elseif ($parameter['type'] === 'call' && $parameter['id'] === $room->getRemoteToken()) {
$parameter['id'] = $room->getToken();
$parameter['icon-url'] = $this->avatarService->getAvatarUrl($room);
if (!isset($parameter['mention-id'])) {
$parameter['mention-id'] = 'all';
}
} elseif ($parameter['type'] === 'circle') {
if (!isset($parameter['mention-id'])) {
$parameter['mention-id'] = 'team/' . $parameter['id'];
}
} elseif ($parameter['type'] === 'user-group') {
if (!isset($parameter['mention-id'])) {
$parameter['mention-id'] = 'group/' . $parameter['id'];
}
} elseif ($parameter['type'] === 'email' || $parameter['type'] === 'guest') {
if (!isset($parameter['mention-id'])) {
$parameter['mention-id'] = $parameter['type'] . '/' . $parameter['id'];
}
}
return $parameter;
}
public function convertMessageParameters(Room $room, array $message): array {
$message['messageParameters'] = array_map(
fn (array $message): array => $this->convertMessageParameter($room, $message),
$message['messageParameters']
);
return $message;
}
/**
* @param Room $room
* @param TalkChatMessageWithParent $message
* @return TalkChatMessageWithParent
*/
public function convertMessage(Room $room, array $message): array {
$message['token'] = $room->getToken();
$message = $this->convertAttendee($room, $message, 'actorType', 'actorId', 'actorDisplayName');
$message = $this->convertAttendee($room, $message, 'lastEditActorType', 'lastEditActorId', 'lastEditActorDisplayName');
$message = $this->convertMessageParameters($room, $message);
if (isset($message['parent'])) {
$message['parent'] = $this->convertMessage($room, $message['parent']);
}
return $message;
}
/**
* @param Room $room
* @param TalkChatMessageWithParent[] $messages
* @return TalkChatMessageWithParent[]
*/
public function convertMessages(Room $room, array $messages): array {
return array_map(
fn (array $message): array => $this->convertMessage($room, $message),
$messages
);
}
/**
* @param Room $room
* @param TalkThreadInfo $threadInfo
* @return TalkThreadInfo
*/
public function convertThreadInfo(Room $room, array $threadInfo): array {
$threadInfo['thread']['roomToken'] = $room->getToken();
if (isset($threadInfo['first'])) {
$threadInfo['first'] = $this->convertMessageParameters($room, $threadInfo['first']);
}
if (isset($threadInfo['last'])) {
$threadInfo['last'] = $this->convertMessageParameters($room, $threadInfo['last']);
}
return $threadInfo;
}
/**
* @param Room $room
* @param list<TalkThreadInfo> $threadInfos
* @return list<TalkThreadInfo>
*/
public function convertThreadInfos(Room $room, array $threadInfos): array {
return array_map(
fn (array $threadInfo): array => $this->convertThreadInfo($room, $threadInfo),
$threadInfos
);
}
/**
* @template T of TalkPoll|TalkPollDraft
* @param Room $room
* @param TalkPoll|TalkPollDraft $poll
* @psalm-param T $poll
* @return TalkPoll|TalkPollDraft
* @psalm-return T
*/
public function convertPoll(Room $room, array $poll): array {
$poll = $this->convertAttendee($room, $poll, 'actorType', 'actorId', 'actorDisplayName');
if (isset($poll['details'])) {
$poll['details'] = array_map(
fn (array $vote): array => $this->convertAttendee($room, $vote, 'actorType', 'actorId', 'actorDisplayName'),
$poll['details']
);
}
return $poll;
}
/**
* @param Room $room
* @param TalkReaction[] $reactions
* @return TalkReaction[]
*/
protected function convertReactions(Room $room, array $reactions): array {
return array_map(
fn (array $reaction): array => $this->convertAttendee($room, $reaction, 'actorType', 'actorId', 'actorDisplayName'),
$reactions
);
}
/**
* @param Room $room
* @param array<string, TalkReaction[]> $reactionsList
* @return array<string, TalkReaction[]>
*/
public function convertReactionsList(Room $room, array $reactionsList): array {
return array_map(
fn (array $reactions): array => $this->convertReactions($room, $reactions),
$reactionsList
);
}
/**
* @return array<string, array{userId: string, displayName: string}>
*/
protected function getLocalParticipants(Room $room): array {
if (array_key_exists($room->getToken(), $this->participantsPerRoom)) {
return $this->participantsPerRoom[$room->getToken()];
}
$this->participantsPerRoom[$room->getToken()] = [];
$localParticipants = $this->participantService->getActorsByType($room, Attendee::ACTOR_USERS);
foreach ($localParticipants as $participant) {
$this->participantsPerRoom[$room->getToken()][$participant->getInvitedCloudId()] = [
'userId' => $participant->getActorId(),
'displayName' => $participant->getDisplayName(),
];
}
return $this->participantsPerRoom[$room->getToken()];
}
protected function createCloudIdFromUserIdAndFullServerUrl(string $userId, string $serverUrl): string {
if (str_starts_with($serverUrl, 'https://')) {
$serverUrl = substr($serverUrl, strlen('https://'));
}
return $userId . '@' . $serverUrl;
}
}