diff --git a/docs/call.md b/docs/call.md index 5711ed26dfc..99e9bbca081 100644 --- a/docs/call.md +++ b/docs/call.md @@ -60,6 +60,7 @@ ## Send call notification * Required capability: `send-call-notification` +* Federation capability: `federation-v2` * Method: `POST` * Endpoint: `/call/{token}/ring/{attendeeId}` * Data: diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index b1396220c67..5224bdd14df 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -248,11 +248,18 @@ public function joinFederatedCall(string $sessionId, ?int $flags = null, bool $s * 400: Ringing attendee is not possible * 404: Attendee could not be found */ + #[FederationSupported] #[PublicPage] #[RequireCallEnabled] #[RequireParticipant] #[RequirePermission(permission: RequirePermission::START_CALL)] public function ringAttendee(int $attendeeId): DataResponse { + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\CallController::class); + return $proxy->ringAttendee($this->room, $this->participant, $attendeeId); + } + if ($this->room->getCallFlag() === Participant::FLAG_DISCONNECTED) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } diff --git a/lib/Events/AParticipantModifiedEvent.php b/lib/Events/AParticipantModifiedEvent.php index e7e8594f54d..a45f51cb506 100644 --- a/lib/Events/AParticipantModifiedEvent.php +++ b/lib/Events/AParticipantModifiedEvent.php @@ -15,6 +15,7 @@ abstract class AParticipantModifiedEvent extends ARoomEvent { public const PROPERTY_IN_CALL = 'inCall'; public const PROPERTY_NAME = 'name'; public const PROPERTY_PERMISSIONS = 'permissions'; + public const PROPERTY_RESEND_CALL = 'resend_call_notification'; public const PROPERTY_TYPE = 'type'; public const DETAIL_IN_CALL_SILENT = 'silent'; diff --git a/lib/Events/CallNotificationSendEvent.php b/lib/Events/CallNotificationSendEvent.php index 731349ee6de..b14b90ea322 100644 --- a/lib/Events/CallNotificationSendEvent.php +++ b/lib/Events/CallNotificationSendEvent.php @@ -18,13 +18,13 @@ class CallNotificationSendEvent extends ARoomEvent { public function __construct( Room $room, - protected Participant $actor, + protected ?Participant $actor, protected Participant $target, ) { parent::__construct($room); } - public function getActor(): Participant { + public function getActor(): ?Participant { return $this->actor; } diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 43c7a8e3271..2e04da6605f 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -17,6 +17,7 @@ use OCA\Talk\Events\AParticipantModifiedEvent; use OCA\Talk\Events\ARoomModifiedEvent; use OCA\Talk\Events\AttendeesAddedEvent; +use OCA\Talk\Events\CallNotificationSendEvent; use OCA\Talk\Exceptions\CannotReachRemoteException; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; @@ -324,6 +325,9 @@ private function participantModified(int $remoteAttendeeId, array $notification) if ($notification['changedProperty'] === AParticipantModifiedEvent::PROPERTY_PERMISSIONS) { $this->participantService->updatePermissions($room, $participant, Attendee::PERMISSIONS_MODIFY_SET, $notification['newValue']); + } elseif ($notification['changedProperty'] === AParticipantModifiedEvent::PROPERTY_RESEND_CALL) { + $event = new CallNotificationSendEvent($room, null, $participant); + $this->dispatcher->dispatchTyped($event); } else { $this->logger->debug('Update of participant property "' . $notification['changedProperty'] . '" is not handled and should not be send via federation'); } diff --git a/lib/Federation/Proxy/TalkV1/Controller/CallController.php b/lib/Federation/Proxy/TalkV1/Controller/CallController.php index 5cb8f79cd90..916666bd4eb 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/CallController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/CallController.php @@ -104,6 +104,36 @@ public function joinFederatedCall(Room $room, Participant $participant, int $fla return new DataResponse([], $statusCode); } + /** + * @see \OCA\Talk\Controller\RoomController::ringAttendee() + * + * @param int $attendeeId ID of the attendee to ring + * @return DataResponse, array{}>|DataResponse + * @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(); + } + + /** @var array{error?: string} $data */ + $data = $this->proxy->getOCSData($proxy); + + return new DataResponse($data, $statusCode); + } + /** * @see \OCA\Talk\Controller\RoomController::updateFederatedCallFlags() * diff --git a/lib/Federation/Proxy/TalkV1/Notifier/ParticipantModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/ParticipantModifiedListener.php index cacbccace12..113945c9429 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/ParticipantModifiedListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/ParticipantModifiedListener.php @@ -43,6 +43,7 @@ public function handle(Event $event): void { if (!in_array($event->getProperty(), [ AParticipantModifiedEvent::PROPERTY_PERMISSIONS, + AParticipantModifiedEvent::PROPERTY_RESEND_CALL, ], true)) { return; } diff --git a/lib/Notification/Listener.php b/lib/Notification/Listener.php index a03943f5434..00cee2a1933 100644 --- a/lib/Notification/Listener.php +++ b/lib/Notification/Listener.php @@ -57,7 +57,7 @@ public function __construct( public function handle(Event $event): void { match (get_class($event)) { - CallNotificationSendEvent::class => $this->sendCallNotification($event->getRoom(), $event->getActor()->getAttendee(), $event->getTarget()->getAttendee()), + CallNotificationSendEvent::class => $this->sendCallNotification($event->getRoom(), $event->getActor()?->getAttendee(), $event->getTarget()->getAttendee()), AttendeesAddedEvent::class => $this->generateInvitation($event->getRoom(), $event->getAttendees()), UserJoinedRoomEvent::class => $this->handleUserJoinedRoomEvent($event), BeforeCallStartedEvent::class => $this->checkCallNotifications($event), @@ -335,7 +335,7 @@ protected function sendCallNotifications(Room $room): void { /** * Forced call notification when ringing a single participant again */ - protected function sendCallNotification(Room $room, Attendee $actor, Attendee $target): void { + protected function sendCallNotification(Room $room, ?Attendee $actor, Attendee $target): void { try { // Remove previous call notifications $notification = $this->notificationManager->createNotification(); @@ -346,7 +346,7 @@ protected function sendCallNotification(Room $room, Attendee $actor, Attendee $t $dateTime = $this->timeFactory->getDateTime(); $notification->setSubject('call', [ - 'callee' => $actor->getActorId(), + 'callee' => $actor?->getActorId(), ]) ->setDateTime($dateTime); $this->notificationManager->notify($notification); diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 9e4e27983a0..198a4c8eb58 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -999,7 +999,7 @@ protected function parseCall(INotification $notification, Room $room, IL10N $l): $roomName = $room->getDisplayName($notification->getUser()); if ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) { $parameters = $notification->getSubjectParameters(); - $calleeId = $parameters['callee']; + $calleeId = $parameters['callee']; // TODO can be null on federated conversations, so needs to be changed once we have federated 1-1 $userDisplayName = $this->userManager->getDisplayName($calleeId); if ($userDisplayName !== null) { if ($this->notificationManager->isPreparingPushNotification() || $this->participantService->hasActiveSessionsInCall($room)) { diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 6292a5fe96e..b89d2e9e257 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -1223,6 +1223,12 @@ public function changeInCall(Room $room, Participant $participant, int $flags, b */ public function sendCallNotificationForAttendee(Room $room, Participant $currentParticipant, int $targetAttendeeId): void { $attendee = $this->attendeeMapper->getById($targetAttendeeId); + if ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { + $target = new Participant($room, $attendee, null); + $event = new ParticipantModifiedEvent($room, $target, AParticipantModifiedEvent::PROPERTY_RESEND_CALL, 1); + $this->dispatcher->dispatchTyped($event); + return; + } if ($attendee->getActorType() !== Attendee::ACTOR_USERS) { throw new \InvalidArgumentException('actor-type'); } diff --git a/src/components/RightSidebar/Participants/Participant.vue b/src/components/RightSidebar/Participants/Participant.vue index 5d956732511..e48a2809146 100644 --- a/src/components/RightSidebar/Participants/Participant.vue +++ b/src/components/RightSidebar/Participants/Participant.vue @@ -610,7 +610,7 @@ export default { }, canSendCallNotification() { - return this.isUserActor + return (this.isUserActor || this.isFederatedActor) && !this.isSelf && (this.currentParticipant.permissions & PARTICIPANT.PERMISSIONS.CALL_START) !== 0 // Can also be undefined, so have to check > than disconnect diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 678ba40d37f..213259bc820 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -144,6 +144,19 @@ public static function getSessionIdForUser(string $user): string { } public function getAttendeeId(string $type, string $id, string $room, ?string $user = null) { + if ($type === 'federated_users') { + if (!str_contains($id, '@')) { + $id .= '@' . $this->localRemoteServerUrl; + } else { + $id = str_replace( + ['LOCAL', 'REMOTE'], + [$this->localServerUrl, $this->remoteServerUrl], + $id + ); + } + $id = rtrim($id, '/'); + } + if (!isset(self::$userToAttendeeId[$room][$type][$id])) { if ($user !== null) { $this->userLoadsAttendeeIdsInRoom($user, $room, 'v4'); @@ -2114,7 +2127,7 @@ public function userUpdatesCallFlagsInRoomTo(string $user, string $identifier, s } /** - * @Then /^user "([^"]*)" pings (user|guest) "([^"]*)"( attendeeIdPlusOne)? to join call "([^"]*)" with (\d+) \((v4)\)$/ + * @Then /^user "([^"]*)" pings (federated_user|user|guest) "([^"]*)"( attendeeIdPlusOne)? to join call "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $actorType @@ -2126,7 +2139,7 @@ public function userUpdatesCallFlagsInRoomTo(string $user, string $identifier, s public function userPingsAttendeeInRoomTo(string $user, string $actorType, string $actorId, ?string $offset, string $identifier, int $statusCode, string $apiVersion): void { $this->setCurrentUser($user); - $attendeeId = self::$userToAttendeeId[$identifier][$actorType . 's'][$actorId]; + $attendeeId = $this->getAttendeeId($actorType . 's', $actorId, $identifier, $user); if ($offset) { $attendeeId++; } diff --git a/tests/integration/features/federation/call.feature b/tests/integration/features/federation/call.feature index 4ccda173d80..4ba6f97ea31 100644 --- a/tests/integration/features/federation/call.feature +++ b/tests/integration/features/federation/call.feature @@ -430,3 +430,65 @@ Feature: federation/call | type | name | recordingConsent | | 2 | room | 0 | When user "participant1" leaves call "room" with 200 (v4) + + Scenario: Resend call notification for federated user + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + When user "participant1" joins call "room" with 200 (v4) + Then using server "REMOTE" + And user "participant2" has the following notifications + | app | object_type | object_id | subject | + | spreed | call | LOCAL::room | A group call has started in room | + And user "participant2" joins room "LOCAL::room" with 200 (v4) + When user "participant2" joins call "LOCAL::room" with 200 (v4) + When user "participant2" leaves call "LOCAL::room" with 200 (v4) + And user "participant2" has the following notifications + And using server "LOCAL" + Then user "participant1" loads attendees attendee ids in room "room" (v4) + Then user "participant1" pings federated_user "participant2@REMOTE" to join call "room" with 200 (v4) + Then using server "REMOTE" + And user "participant2" has the following notifications + | app | object_type | object_id | subject | + | spreed | call | LOCAL::room | A group call has started in room | + + Scenario: Resend call notification as a federated user + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2@REMOTE" to room "room" with 200 (v4) + And using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + And user "participant2" joins room "LOCAL::room" with 200 (v4) + When user "participant2" joins call "LOCAL::room" with 200 (v4) + And using server "LOCAL" + And user "participant1" has the following notifications + | app | object_type | object_id | subject | + | spreed | call | room | A group call has started in room | + And user "participant1" joins room "room" with 200 (v4) + When user "participant1" joins call "room" with 200 (v4) + When user "participant1" leaves call "room" with 200 (v4) + And user "participant1" has the following notifications + Then using server "REMOTE" + Then user "participant2" loads attendees attendee ids in room "LOCAL::room" (v4) + Then user "participant2" pings federated_user "participant1@LOCAL" to join call "LOCAL::room" with 200 (v4) + And using server "LOCAL" + And user "participant1" has the following notifications + | app | object_type | object_id | subject | + | spreed | call | room | A group call has started in room |