diff --git a/docs/events.md b/docs/events.md index 89040ac1512..e09e956b1ea 100644 --- a/docs/events.md +++ b/docs/events.md @@ -34,11 +34,30 @@ See the general [Nextcloud Developers - Events](https://docs.nextcloud.com/serve * After event: `OCA\Talk\Events\LobbyModifiedEvent` * Since: 18.0.0 +### Call started + +* Before event: `OCA\Talk\Events\BeforeCallStartedEvent` +* After event: `OCA\Talk\Events\CallStartedEvent` + The after event might be skipped if the request lost the race to update the database. + A parallel request will have triggered the before and after events in the meantime. +* Since: 20.0.0 + ### Call ended for everyone * Before event: `OCA\Talk\Events\BeforeCallEndedForEveryoneEvent` * After event: `OCA\Talk\Events\CallEndedForEveryoneEvent` * Since: 18.0.0 +* Since: 20.0.0 Extends the abstract `ACallEndedEvent` + +### Call ended + +When the last participant is leaving the call, the session expired or the participant was removed. + +* Before event: `OCA\Talk\Events\BeforeCallEndedEvent` +* After event: `OCA\Talk\Events\CallEndedEvent` + The after event might be skipped if the request lost the race to update the database. + A parallel request will have triggered the before and after events in the meantime. +* Since: 20.0.0 ### Conversation password verify @@ -47,12 +66,6 @@ Allows to verify a password and set a redirect URL for the invalid case * Event: `OCA\Talk\Events\RoomPasswordVerifyEvent` * Since: 18.0.0 -### Active since modified - -* Before event: `OCA\Talk\Events\BeforeActiveSinceModifiedEvent` -* After event: `OCA\Talk\Events\ActiveSinceModifiedEvent` -* Since: 20.0.0 - ## Participant related events ### Attendees added diff --git a/lib/Activity/Listener.php b/lib/Activity/Listener.php index ca22fe06b19..1b60b6d3061 100644 --- a/lib/Activity/Listener.php +++ b/lib/Activity/Listener.php @@ -9,19 +9,18 @@ namespace OCA\Talk\Activity; use OCA\Talk\Chat\ChatManager; -use OCA\Talk\Events\AParticipantModifiedEvent; +use OCA\Talk\Events\ACallEndedEvent; use OCA\Talk\Events\ARoomEvent; -use OCA\Talk\Events\AttendeeRemovedEvent; use OCA\Talk\Events\AttendeesAddedEvent; -use OCA\Talk\Events\BeforeCallEndedForEveryoneEvent; -use OCA\Talk\Events\ParticipantModifiedEvent; -use OCA\Talk\Events\SessionLeftRoomEvent; +use OCA\Talk\Events\CallEndedEvent; +use OCA\Talk\Events\CallEndedForEveryoneEvent; use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCA\Talk\Service\RecordingService; use OCA\Talk\Service\RoomService; +use OCP\Activity\Exceptions\InvalidValueException; use OCP\Activity\IManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\Event; @@ -53,85 +52,31 @@ public function handle(Event $event): void { } match (get_class($event)) { - BeforeCallEndedForEveryoneEvent::class => $this->generateCallActivity($event->getRoom(), true, $event->getActor()), - SessionLeftRoomEvent::class, - AttendeeRemovedEvent::class => $this->generateCallActivity($event->getRoom()), - ParticipantModifiedEvent::class => $this->handleParticipantModified($event), + CallEndedEvent::class, + CallEndedForEveryoneEvent::class => $this->generateCallActivity($event), AttendeesAddedEvent::class => $this->generateInvitationActivity($event->getRoom(), $event->getAttendees()), }; } - protected function setActive(ParticipantModifiedEvent $event): void { - if ($event->getProperty() !== AParticipantModifiedEvent::PROPERTY_IN_CALL) { - return; - } - - if ($event->getOldValue() !== Participant::FLAG_DISCONNECTED - || $event->getNewValue() === Participant::FLAG_DISCONNECTED) { - return; - } - - $participant = $event->getParticipant(); - $this->roomService->setActiveSince( - $event->getRoom(), - $this->timeFactory->getDateTime(), - $participant->getSession() ? $participant->getSession()->getInCall() : Participant::FLAG_DISCONNECTED - ); - } - - protected function handleParticipantModified(ParticipantModifiedEvent $event): void { - if ($event->getProperty() !== AParticipantModifiedEvent::PROPERTY_IN_CALL) { - return; - } - - if ($event->getOldValue() === Participant::FLAG_DISCONNECTED - || $event->getNewValue() !== Participant::FLAG_DISCONNECTED) { - $this->setActive($event); - return; - } - - if ($event->getDetail(AParticipantModifiedEvent::DETAIL_IN_CALL_END_FOR_EVERYONE)) { - // The call activity was generated already if the call is ended - // for everyone - return; - } - - $this->generateCallActivity($event->getRoom()); - } - /** * Call activity: "You attended a call with {user1} and {user2}" - * - * @param Room $room - * @param bool $endForEveryone - * @param Participant|null $actor - * @return bool True if activity was generated, false otherwise */ - protected function generateCallActivity(Room $room, bool $endForEveryone = false, ?Participant $actor = null): bool { - $activeSince = $room->getActiveSince(); - if (!$activeSince instanceof \DateTime || (!$endForEveryone && $this->participantService->hasActiveSessionsInCall($room))) { - return false; - } + protected function generateCallActivity(ACallEndedEvent $event): void { + $room = $event->getRoom(); + $actor = $event->getActor(); + $activeSince = $event->getOldValue(); $duration = $this->timeFactory->getTime() - $activeSince->getTimestamp(); $userIds = $this->participantService->getParticipantUserIds($room, $activeSince); $numGuests = $this->participantService->getGuestCount($room, $activeSince); $message = 'call_ended'; - if ($endForEveryone) { + if ($event instanceof CallEndedForEveryoneEvent) { $message = 'call_ended_everyone'; } elseif (($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) && \count($userIds) === 1) { $message = 'call_missed'; } - if (!$this->roomService->resetActiveSince($room)) { - // Race-condition, the room was already reset. - return false; - } - - if ($room->getCallRecording() !== Room::RECORDING_NONE && $room->getCallRecording() !== Room::RECORDING_FAILED) { - $this->recordingService->stop($room); - } if ($actor instanceof Participant) { $actorId = $actor->getAttendee()->getActorId(); $actorType = $actor->getAttendee()->getActorType(); @@ -149,12 +94,12 @@ protected function generateCallActivity(Room $room, bool $endForEveryone = false ]), $this->timeFactory->getDateTime(), false); if (empty($userIds)) { - return false; + return; } - $event = $this->activityManager->generateEvent(); + $activity = $this->activityManager->generateEvent(); try { - $event->setApp('spreed') + $activity->setApp('spreed') ->setType('spreed') ->setAuthor('') ->setObject('room', $room->getId()) @@ -165,21 +110,19 @@ protected function generateCallActivity(Room $room, bool $endForEveryone = false 'guests' => $numGuests, 'duration' => $duration, ]); - } catch (\InvalidArgumentException $e) { + } catch (InvalidValueException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - return false; + return; } foreach ($userIds as $userId) { try { - $event->setAffectedUser($userId); - $this->activityManager->publish($event); + $activity->setAffectedUser($userId); + $this->activityManager->publish($activity); } catch (\Throwable $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); } } - - return true; } /** diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 2746aba0a5a..f145a74053b 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -30,13 +30,12 @@ use OCA\Talk\Config; use OCA\Talk\Dashboard\TalkWidget; use OCA\Talk\Deck\DeckPluginLoader; -use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\AttendeeRemovedEvent; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Events\AttendeesRemovedEvent; use OCA\Talk\Events\BeforeAttendeeRemovedEvent; use OCA\Talk\Events\BeforeAttendeesAddedEvent; -use OCA\Talk\Events\BeforeCallEndedForEveryoneEvent; +use OCA\Talk\Events\BeforeCallStartedEvent; use OCA\Talk\Events\BeforeDuplicateShareSentEvent; use OCA\Talk\Events\BeforeGuestJoinedRoomEvent; use OCA\Talk\Events\BeforeParticipantModifiedEvent; @@ -48,8 +47,10 @@ use OCA\Talk\Events\BotEnabledEvent; use OCA\Talk\Events\BotInstallEvent; use OCA\Talk\Events\BotUninstallEvent; +use OCA\Talk\Events\CallEndedEvent; use OCA\Talk\Events\CallEndedForEveryoneEvent; use OCA\Talk\Events\CallNotificationSendEvent; +use OCA\Talk\Events\CallStartedEvent; use OCA\Talk\Events\ChatMessageSentEvent; use OCA\Talk\Events\EmailInvitationSentEvent; use OCA\Talk\Events\GuestJoinedRoomEvent; @@ -171,10 +172,8 @@ public function register(IRegistrationContext $context): void { // Activity listeners $context->registerEventListener(AttendeesAddedEvent::class, ActivityListener::class); - $context->registerEventListener(AttendeeRemovedEvent::class, ActivityListener::class); - $context->registerEventListener(BeforeCallEndedForEveryoneEvent::class, ActivityListener::class); - $context->registerEventListener(ParticipantModifiedEvent::class, ActivityListener::class, 75); - $context->registerEventListener(SessionLeftRoomEvent::class, ActivityListener::class, -100); + $context->registerEventListener(CallEndedEvent::class, ActivityListener::class); + $context->registerEventListener(CallEndedForEveryoneEvent::class, ActivityListener::class); // Bot listeners $context->registerEventListener(BotDisabledEvent::class, BotListener::class); @@ -246,8 +245,8 @@ public function register(IRegistrationContext $context): void { // Notification listeners $context->registerEventListener(AttendeesAddedEvent::class, NotificationListener::class); - $context->registerEventListener(ActiveSinceModifiedEvent::class, NotificationListener::class); - $context->registerEventListener(BeforeParticipantModifiedEvent::class, NotificationListener::class); + $context->registerEventListener(BeforeCallStartedEvent::class, NotificationListener::class); + $context->registerEventListener(CallStartedEvent::class, NotificationListener::class); $context->registerEventListener(CallNotificationSendEvent::class, NotificationListener::class); $context->registerEventListener(ParticipantModifiedEvent::class, NotificationListener::class); $context->registerEventListener(UserJoinedRoomEvent::class, NotificationListener::class); @@ -260,12 +259,16 @@ public function register(IRegistrationContext $context): void { // Recording listeners $context->registerEventListener(RoomDeletedEvent::class, RecordingListener::class); + $context->registerEventListener(CallEndedEvent::class, RecordingListener::class); + $context->registerEventListener(CallEndedForEveryoneEvent::class, RecordingListener::class); $context->registerEventListener(TranscriptionSuccessfulEvent::class, RecordingListener::class); $context->registerEventListener(TranscriptionFailedEvent::class, RecordingListener::class); // Federation listeners $context->registerEventListener(BeforeRoomDeletedEvent::class, TalkV1BeforeRoomDeletedListener::class); - $context->registerEventListener(ActiveSinceModifiedEvent::class, TalkV1RoomModifiedListener::class); + $context->registerEventListener(CallEndedEvent::class, TalkV1RoomModifiedListener::class); + $context->registerEventListener(CallEndedForEveryoneEvent::class, TalkV1RoomModifiedListener::class); + $context->registerEventListener(CallStartedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(LobbyModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(RoomModifiedEvent::class, TalkV1RoomModifiedListener::class); $context->registerEventListener(ChatMessageSentEvent::class, TalkV1MessageSentListener::class); diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 0977dd4ba9e..07eb3b62f7d 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -163,10 +163,11 @@ public function joinCall(?int $flags = null, ?int $forcePermissions = null, bool $this->roomService->setPermissions($this->room, 'call', Attendee::PERMISSIONS_MODIFY_SET, $forcePermissions, true); } - $joined = $this->participantService->changeInCall($this->room, $this->participant, $flags, false, $silent); - - if (!$joined) { - return new DataResponse([], Http::STATUS_BAD_REQUEST); + try { + $this->participantService->changeInCall($this->room, $this->participant, $flags, silent: $silent); + $this->roomService->setActiveSince($this->room, $this->participant, $this->timeFactory->getDateTime(), $flags, silent: $silent); + } catch (\InvalidArgumentException $e) { + return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } return new DataResponse(); } @@ -226,8 +227,10 @@ public function joinFederatedCall(string $sessionId, ?int $flags = null, bool $s return new DataResponse(['error' => 'consent'], Http::STATUS_BAD_REQUEST); } - $joined = $this->participantService->changeInCall($this->room, $this->participant, $flags, false, $silent); - if (!$joined) { + try { + $this->participantService->changeInCall($this->room, $this->participant, $flags, false, $silent); + $this->roomService->setActiveSince($this->room, $this->participant, $this->timeFactory->getDateTime(), $flags, silent: $silent); + } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -414,8 +417,12 @@ public function leaveCall(bool $all = false): DataResponse { if ($all && $this->participant->hasModeratorPermissions()) { $this->participantService->endCallForEveryone($this->room, $this->participant); + $this->roomService->resetActiveSince($this->room, $this->participant, true); } else { $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); + if (!$this->participantService->hasActiveSessionsInCall($this->room)) { + $this->roomService->resetActiveSince($this->room, $this->participant); + } } return new DataResponse(); @@ -443,6 +450,9 @@ public function leaveFederatedCall(string $sessionId): DataResponse { } $this->participantService->changeInCall($this->room, $this->participant, Participant::FLAG_DISCONNECTED); + if (!$this->participantService->hasActiveSessionsInCall($this->room)) { + $this->roomService->resetActiveSince($this->room, $this->participant); + } return new DataResponse(); } diff --git a/lib/Events/AActiveSinceModifiedEvent.php b/lib/Events/ACallEndedEvent.php similarity index 55% rename from lib/Events/AActiveSinceModifiedEvent.php rename to lib/Events/ACallEndedEvent.php index 29083be46b7..08189c2e0ac 100644 --- a/lib/Events/AActiveSinceModifiedEvent.php +++ b/lib/Events/ACallEndedEvent.php @@ -8,29 +8,28 @@ namespace OCA\Talk\Events; +use OCA\Talk\Participant; use OCA\Talk\Room; -abstract class AActiveSinceModifiedEvent extends ARoomModifiedEvent { +/** + * @psalm-method \DateTime getOldValue() + */ +abstract class ACallEndedEvent extends ARoomModifiedEvent { public function __construct( Room $room, - ?\DateTime $newValue, - ?\DateTime $oldValue, - protected int $callFlag, - protected int $oldCallFlag, + ?Participant $actor, + \DateTime $oldActiveSince, ) { parent::__construct( $room, self::PROPERTY_ACTIVE_SINCE, - $newValue, - $oldValue, + null, + $oldActiveSince, + $actor ); } public function getCallFlag(): int { - return $this->callFlag; - } - - public function getOldCallFlag(): int { - return $this->oldCallFlag; + return Participant::FLAG_DISCONNECTED; } } diff --git a/lib/Events/ACallEndedForEveryoneEvent.php b/lib/Events/ACallEndedForEveryoneEvent.php index 78eaa4b9418..60a12814fb5 100644 --- a/lib/Events/ACallEndedForEveryoneEvent.php +++ b/lib/Events/ACallEndedForEveryoneEvent.php @@ -8,20 +8,5 @@ namespace OCA\Talk\Events; -use OCA\Talk\Participant; -use OCA\Talk\Room; - -abstract class ACallEndedForEveryoneEvent extends ARoomModifiedEvent { - public function __construct( - Room $room, - ?Participant $actor = null, - ) { - parent::__construct( - $room, - self::PROPERTY_IN_CALL, - Participant::FLAG_DISCONNECTED, - null, - $actor - ); - } +abstract class ACallEndedForEveryoneEvent extends ACallEndedEvent { } diff --git a/lib/Events/ACallStartedEvent.php b/lib/Events/ACallStartedEvent.php new file mode 100644 index 00000000000..32a303156fa --- /dev/null +++ b/lib/Events/ACallStartedEvent.php @@ -0,0 +1,44 @@ + $details + */ + public function __construct( + Room $room, + ?\DateTime $newValue, + protected int $callFlag, + protected array $details, + ?Participant $actor, + ) { + parent::__construct( + $room, + self::PROPERTY_ACTIVE_SINCE, + $newValue, + null, + $actor, + ); + } + + public function getCallFlag(): int { + return $this->callFlag; + } + + /** + * @param AParticipantModifiedEvent::DETAIL_* $detail + */ + public function getDetail(string $detail): ?bool { + return $this->details[$detail] ?? null; + } +} diff --git a/lib/Events/ActiveSinceModifiedEvent.php b/lib/Events/ActiveSinceModifiedEvent.php deleted file mode 100644 index f69f89a71cd..00000000000 --- a/lib/Events/ActiveSinceModifiedEvent.php +++ /dev/null @@ -1,34 +0,0 @@ -updatedActiveSince; - } -} diff --git a/lib/Events/BeforeActiveSinceModifiedEvent.php b/lib/Events/BeforeCallEndedEvent.php similarity index 72% rename from lib/Events/BeforeActiveSinceModifiedEvent.php rename to lib/Events/BeforeCallEndedEvent.php index db101d40ac8..76e9fa3133c 100644 --- a/lib/Events/BeforeActiveSinceModifiedEvent.php +++ b/lib/Events/BeforeCallEndedEvent.php @@ -8,5 +8,5 @@ namespace OCA\Talk\Events; -class BeforeActiveSinceModifiedEvent extends AActiveSinceModifiedEvent { +class BeforeCallEndedEvent extends ACallEndedEvent { } diff --git a/lib/Events/BeforeCallStartedEvent.php b/lib/Events/BeforeCallStartedEvent.php new file mode 100644 index 00000000000..77b3ae88463 --- /dev/null +++ b/lib/Events/BeforeCallStartedEvent.php @@ -0,0 +1,12 @@ + $details */ - public function sendRoomModifiedActiveSinceUpdate( + public function sendCallStarted( string $remoteServer, int $localAttendeeId, #[SensitiveParameter] string $accessToken, string $localToken, string $changedProperty, - ?\DateTime $newValue, - ?\DateTime $oldValue, + \DateTime $activeSince, int $callFlag, + array $details, ): ?bool { $remote = $this->prepareRemoteUrl($remoteServer); - if ($newValue instanceof \DateTime) { - $newValue = (string) $newValue->getTimestamp(); - } - if ($oldValue instanceof \DateTime) { - $oldValue = (string) $oldValue->getTimestamp(); - } + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + FederationManager::NOTIFICATION_ROOM_MODIFIED, + FederationManager::TALK_ROOM_RESOURCE, + (string) $localAttendeeId, + [ + 'remoteServerUrl' => $this->getServerRemoteUrl(), + 'sharedSecret' => $accessToken, + 'remoteToken' => $localToken, + 'changedProperty' => $changedProperty, + 'newValue' => $activeSince->getTimestamp(), + 'oldValue' => null, + 'callFlag' => $callFlag, + 'details' => $details, + ], + ); + + return $this->sendUpdateToRemote($remote, $notification); + } + + /** + * Send information to remote participants that "active since" was updated + * Sent from Host server to Remote participant server + * + * @psalm-param array $details + */ + public function sendCallEnded( + string $remoteServer, + int $localAttendeeId, + #[SensitiveParameter] + string $accessToken, + string $localToken, + string $changedProperty, + ?\DateTime $activeSince, + int $callFlag, + array $details, + ): ?bool { + $remote = $this->prepareRemoteUrl($remoteServer); $notification = $this->cloudFederationFactory->getCloudFederationNotification(); $notification->setMessage( @@ -274,9 +309,10 @@ public function sendRoomModifiedActiveSinceUpdate( 'sharedSecret' => $accessToken, 'remoteToken' => $localToken, 'changedProperty' => $changedProperty, - 'newValue' => $newValue, - 'oldValue' => $oldValue, + 'newValue' => $activeSince?->getTimestamp(), + 'oldValue' => null, 'callFlag' => $callFlag, + 'details' => $details, ], ); diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index c5d205f84c4..e5c9fb93507 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -14,6 +14,7 @@ use OCA\Talk\CachePrefix; use OCA\Talk\Config; use OCA\Talk\Events\AAttendeeRemovedEvent; +use OCA\Talk\Events\AParticipantModifiedEvent; use OCA\Talk\Events\ARoomModifiedEvent; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Exceptions\CannotReachRemoteException; @@ -288,7 +289,7 @@ private function shareUnshared(int $remoteAttendeeId, array $notification): arra /** * @param int $remoteAttendeeId - * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null, callFlag?: int, dateTime?: string, timerReached?: bool} $notification + * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null, callFlag?: int, dateTime?: string, timerReached?: bool, details?: array} $notification * @return array * @throws ActionNotSupportedException * @throws AuthenticationFailedException @@ -309,17 +310,17 @@ private function roomModified(int $remoteAttendeeId, array $notification): array if ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE) { if ($notification['newValue'] === null) { - $this->roomService->resetActiveSince($room); + $this->roomService->resetActiveSince($room, null); } else { - $activeSince = \DateTime::createFromFormat('U', $notification['newValue']); - $this->roomService->setActiveSince($room, $activeSince, $notification['callFlag']); + $activeSince = $this->timeFactory->getDateTime('@' . $notification['newValue']); + $this->roomService->setActiveSince($room, null, $activeSince, $notification['callFlag'], !empty($notification['details'][AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT])); } } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_AVATAR) { $this->roomService->setAvatar($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_DESCRIPTION) { $this->roomService->setDescription($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_IN_CALL) { - $this->roomService->setActiveSince($room, $room->getActiveSince(), $notification['newValue']); + $this->roomService->setActiveSince($room, null, $room->getActiveSince(), $notification['newValue'], true); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_LOBBY) { $dateTime = !empty($notification['dateTime']) ? \DateTime::createFromFormat('U', $notification['dateTime']) : null; $this->roomService->setLobby($room, $notification['newValue'], $dateTime, $notification['timerReached'] ?? false); diff --git a/lib/Federation/Proxy/TalkV1/Controller/CallController.php b/lib/Federation/Proxy/TalkV1/Controller/CallController.php index 59b9ca98b42..30c8d642e04 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/CallController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/CallController.php @@ -40,7 +40,7 @@ public function __construct( * @psalm-param int-mask-of $flags * @param bool $silent Join the call silently * @param bool $recordingConsent Agreement to be recorded - * @return DataResponse, array{}> + * @return DataResponse, array{}>|DataResponse * @throws CannotReachRemoteException * * 200: Federated user is now in the call diff --git a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php index df467a67c69..90d7dc5024f 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php @@ -8,11 +8,13 @@ namespace OCA\Talk\Federation\Proxy\TalkV1\Notifier; -use OCA\Talk\Events\AActiveSinceModifiedEvent; use OCA\Talk\Events\AAttendeeRemovedEvent; -use OCA\Talk\Events\ActiveSinceModifiedEvent; 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; @@ -36,7 +38,9 @@ public function __construct( } public function handle(Event $event): void { - if (!$event instanceof ActiveSinceModifiedEvent + if (!$event instanceof CallStartedEvent + && !$event instanceof CallEndedEvent + && !$event instanceof CallEndedForEveryoneEvent && !$event instanceof LobbyModifiedEvent && !$event instanceof RoomModifiedEvent) { return; @@ -60,8 +64,10 @@ public function handle(Event $event): void { foreach ($participants as $participant) { $cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId()); - if ($event instanceof AActiveSinceModifiedEvent) { - $success = $this->notifyActiveSinceModified($cloudId, $participant, $event); + 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 { @@ -74,16 +80,39 @@ public function handle(Event $event): void { } } - private function notifyActiveSinceModified(ICloudId $cloudId, Participant $participant, AActiveSinceModifiedEvent $event) { - return $this->backendNotifier->sendRoomModifiedActiveSinceUpdate( + 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->getOldValue(), $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, ); } diff --git a/lib/Notification/Listener.php b/lib/Notification/Listener.php index ebf75cd24cd..a03943f5434 100644 --- a/lib/Notification/Listener.php +++ b/lib/Notification/Listener.php @@ -10,11 +10,11 @@ use OCA\Talk\AppInfo\Application; use OCA\Talk\Controller\ChatController; -use OCA\Talk\Events\ActiveSinceModifiedEvent; use OCA\Talk\Events\AParticipantModifiedEvent; use OCA\Talk\Events\AttendeesAddedEvent; -use OCA\Talk\Events\BeforeParticipantModifiedEvent; +use OCA\Talk\Events\BeforeCallStartedEvent; use OCA\Talk\Events\CallNotificationSendEvent; +use OCA\Talk\Events\CallStartedEvent; use OCA\Talk\Events\ParticipantModifiedEvent; use OCA\Talk\Events\UserJoinedRoomEvent; use OCA\Talk\Model\Attendee; @@ -60,9 +60,9 @@ public function handle(Event $event): void { 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), - BeforeParticipantModifiedEvent::class => $this->checkCallNotifications($event), + BeforeCallStartedEvent::class => $this->checkCallNotifications($event), ParticipantModifiedEvent::class => $this->afterParticipantJoinedCall($event), - ActiveSinceModifiedEvent::class => $this->afterActiveSinceModified($event), + CallStartedEvent::class => $this->afterCallStarted($event), }; } @@ -178,29 +178,13 @@ protected function markReactionNotificationsRead(Room $room, IUser $user): void /** * Call notification: "{user} wants to talk with you" */ - protected function checkCallNotifications(BeforeParticipantModifiedEvent $event): void { - if ($event->getProperty() !== AParticipantModifiedEvent::PROPERTY_IN_CALL) { - return; - } - - if ($event->getOldValue() !== Participant::FLAG_DISCONNECTED - || $event->getNewValue() === Participant::FLAG_DISCONNECTED) { - return; - } - + protected function checkCallNotifications(BeforeCallStartedEvent $event): void { if ($event->getDetail(AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT)) { $this->shouldSendCallNotification = false; return; } - $room = $event->getRoom(); - if ($room->getActiveSince() instanceof \DateTime) { - // Call already active => No new notifications - $this->shouldSendCallNotification = false; - return; - } - - if ($room->getObjectType() === Room::OBJECT_TYPE_FILE) { + if ($event->getRoom()->getObjectType() === Room::OBJECT_TYPE_FILE) { $this->shouldSendCallNotification = false; return; } @@ -218,22 +202,18 @@ protected function afterParticipantJoinedCall(ParticipantModifiedEvent $event): return; } + // Purge received call notifications on joining $this->markCallNotificationsRead($event->getRoom()); - if ($this->shouldSendCallNotification) { - $this->sendCallNotifications($event->getRoom()); - } } - protected function afterActiveSinceModified(ActiveSinceModifiedEvent $event): void { - if (!$event->hasUpdatedActiveSince()) { + protected function afterCallStarted(CallStartedEvent $event): void { + if ($event->getDetail(AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT)) { return; } - if (!$event->getRoom()->isFederatedConversation()) { - return; + if ($this->shouldSendCallNotification || $event->getRoom()->isFederatedConversation()) { + $this->sendCallNotifications($event->getRoom()); } - - $this->sendCallNotifications($event->getRoom()); } /** diff --git a/lib/Recording/Listener.php b/lib/Recording/Listener.php index 55bde46b2ef..94c8037f3a1 100644 --- a/lib/Recording/Listener.php +++ b/lib/Recording/Listener.php @@ -10,7 +10,11 @@ namespace OCA\Talk\Recording; use OCA\Talk\AppInfo\Application; +use OCA\Talk\Events\ACallEndedEvent; +use OCA\Talk\Events\CallEndedEvent; +use OCA\Talk\Events\CallEndedForEveryoneEvent; use OCA\Talk\Events\RoomDeletedEvent; +use OCA\Talk\Room; use OCA\Talk\Service\ConsentService; use OCA\Talk\Service\RecordingService; use OCP\EventDispatcher\Event; @@ -33,16 +37,19 @@ public function __construct( } public function handle(Event $event): void { - if ($event instanceof RoomDeletedEvent) { - $this->roomDeleted($event); + if ($event instanceof AbstractTranscriptionEvent) { + $this->handleTranscriptionEvents($event); return; } - if (!($event instanceof AbstractTranscriptionEvent)) { - // Unrelated - return; - } + match (get_class($event)) { + RoomDeletedEvent::class => $this->roomDeleted($event), + CallEndedEvent::class, + CallEndedForEveryoneEvent::class => $this->endRecordingOnCallEnd($event), + }; + } + public function handleTranscriptionEvents(AbstractTranscriptionEvent $event): void { if ($event->getAppId() !== Application::APP_ID) { return; } @@ -81,4 +88,11 @@ protected function failedTranscript(?string $owner, ?File $fileNode): void { protected function roomDeleted(RoomDeletedEvent $event): void { $this->consentService->deleteByRoom($event->getRoom()); } + + protected function endRecordingOnCallEnd(ACallEndedEvent $event): void { + $callRecording = $event->getRoom()->getCallRecording(); + if ($callRecording !== Room::RECORDING_NONE && $callRecording !== Room::RECORDING_FAILED) { + $this->recordingService->stop($event->getRoom()); + } + } } diff --git a/lib/Room.php b/lib/Room.php index fca31047949..40bbc7ca8b7 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -296,6 +296,7 @@ public function setDescription(string $description): void { public function resetActiveSince(): void { $this->activeSince = null; + $this->callFlag = Participant::FLAG_DISCONNECTED; } public function getDefaultPermissions(): int { diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 5d0daad5561..13249741675 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -840,6 +840,8 @@ public function leaveRoomAsSession(Room $room, Participant $participant, bool $d $user = $this->userManager->get($participant->getAttendee()->getActorId()); $this->removeUser($room, $user, AAttendeeRemovedEvent::REASON_LEFT); + } else { + $this->resetCallStateWhenNeeded($room); } } @@ -890,6 +892,8 @@ public function removeAttendee(Room $room, Participant $participant, string $rea } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_CIRCLES) { $this->removeCircleMembers($room, $participant, $reason); } + + $this->resetCallStateWhenNeeded($room); } /** @@ -1028,6 +1032,8 @@ public function removeUser(Room $room, IUser $user, string $reason): void { $attendeeEvent = new AttendeesRemovedEvent($room, [$attendee]); $this->dispatcher->dispatchTyped($attendeeEvent); + + $this->resetCallStateWhenNeeded($room); } public function cleanGuestParticipants(Room $room): void { @@ -1093,17 +1099,20 @@ public function cleanGuestParticipants(Room $room): void { $event = new GuestsCleanedUpEvent($room); $this->dispatcher->dispatchTyped($event); + + $this->resetCallStateWhenNeeded($room); } public function endCallForEveryone(Room $room, Participant $moderator): void { - $event = new BeforeCallEndedForEveryoneEvent($room, $moderator); + $oldActiveSince = $room->getActiveSince(); + $event = new BeforeCallEndedForEveryoneEvent($room, $moderator, $oldActiveSince); $this->dispatcher->dispatchTyped($event); $participants = $this->getParticipantsInCall($room); $changedSessionIds = []; $changedUserIds = []; - // kick out all participants out of the call + // kick all participants out of the call foreach ($participants as $participant) { $changedSessionIds[] = $participant->getSession()->getSessionId(); if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { @@ -1114,20 +1123,24 @@ public function endCallForEveryone(Room $room, Participant $moderator): void { $this->sessionMapper->resetInCallByIds($changedSessionIds); - $event = new CallEndedForEveryoneEvent($room, $moderator, $changedSessionIds, $changedUserIds); + $event = new CallEndedForEveryoneEvent($room, $moderator, $oldActiveSince, $changedSessionIds, $changedUserIds); $this->dispatcher->dispatchTyped($event); } - public function changeInCall(Room $room, Participant $participant, int $flags, bool $endCallForEveryone = false, bool $silent = false): bool { + /** + * @psalm-param int-mask-of $flags + * @throws \InvalidArgumentException + */ + public function changeInCall(Room $room, Participant $participant, int $flags, bool $endCallForEveryone = false, bool $silent = false): void { if ($room->getType() === Room::TYPE_CHANGELOG || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER || $room->getType() === Room::TYPE_NOTE_TO_SELF) { - return false; + throw new \InvalidArgumentException('type'); } $session = $participant->getSession(); if (!$session instanceof Session) { - return false; + throw new \InvalidArgumentException('session'); } $permissions = $participant->getPermissions(); @@ -1166,8 +1179,6 @@ public function changeInCall(Room $room, Participant $participant, int $flags, b $event = new ParticipantModifiedEvent($room, $participant, AParticipantModifiedEvent::PROPERTY_IN_CALL, $flags, $oldFlags, $details); $this->dispatcher->dispatchTyped($event); - - return true; } /** @@ -1295,6 +1306,8 @@ public function updateCallFlags(Room $room, Participant $participant, int $flags $session->setInCall($flags); $this->sessionMapper->update($session); + // FIXME Missing potential update of call flags on room level + $event = new ParticipantModifiedEvent($room, $participant, AParticipantModifiedEvent::PROPERTY_IN_CALL, $flags, $oldFlags); $this->dispatcher->dispatchTyped($event); } @@ -1782,6 +1795,21 @@ public function cacheParticipant(Room $room, Participant $participant): void { } } + protected function resetCallStateWhenNeeded(Room $room): void { + if ($room->getCallFlag() === Participant::FLAG_DISCONNECTED) { + // No call + return; + } + + if ($this->hasActiveSessionsInCall($room)) { + // Still others there + return; + } + + $roomService = Server::get(RoomService::class); + $roomService->resetActiveSince($room, null); + } + /** * @param Room $room * @return bool diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 33a83320b73..4883a1cfa02 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -10,12 +10,15 @@ use InvalidArgumentException; use OCA\Talk\Config; -use OCA\Talk\Events\ActiveSinceModifiedEvent; +use OCA\Talk\Events\AParticipantModifiedEvent; use OCA\Talk\Events\ARoomModifiedEvent; -use OCA\Talk\Events\BeforeActiveSinceModifiedEvent; +use OCA\Talk\Events\BeforeCallEndedEvent; +use OCA\Talk\Events\BeforeCallStartedEvent; use OCA\Talk\Events\BeforeLobbyModifiedEvent; use OCA\Talk\Events\BeforeRoomDeletedEvent; use OCA\Talk\Events\BeforeRoomModifiedEvent; +use OCA\Talk\Events\CallEndedEvent; +use OCA\Talk\Events\CallStartedEvent; use OCA\Talk\Events\LobbyModifiedEvent; use OCA\Talk\Events\RoomDeletedEvent; use OCA\Talk\Events\RoomModifiedEvent; @@ -833,16 +836,18 @@ public function setBreakoutRoomStatus(Room $room, int $status): bool { return true; } - public function resetActiveSince(Room $room): bool { + public function resetActiveSince(Room $room, ?Participant $participant, bool $alreadyTriggeredCallEndedForEveryone = false): void { $oldActiveSince = $room->getActiveSince(); $oldCallFlag = $room->getCallFlag(); - if ($oldActiveSince === null && $oldCallFlag === Participant::FLAG_DISCONNECTED) { - return false; - } + if (!$alreadyTriggeredCallEndedForEveryone) { + if ($oldActiveSince === null && $oldCallFlag === Participant::FLAG_DISCONNECTED) { + return; + } - $event = new BeforeActiveSinceModifiedEvent($room, null, $oldActiveSince, Participant::FLAG_DISCONNECTED, $oldCallFlag); - $this->dispatcher->dispatchTyped($event); + $event = new BeforeCallEndedEvent($room, $participant, $oldActiveSince); + $this->dispatcher->dispatchTyped($event); + } $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') @@ -852,29 +857,43 @@ public function resetActiveSince(Room $room): bool { ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) ->andWhere($update->expr()->isNotNull('active_since')); + $result = (bool) $update->executeStatement(); + $room->resetActiveSince(); + $room->setCallPermissions(Attendee::PERMISSIONS_DEFAULT); - $result = (bool) $update->executeStatement(); + if ($alreadyTriggeredCallEndedForEveryone) { + return; + } - $event = new ActiveSinceModifiedEvent($room, null, $oldActiveSince, Participant::FLAG_DISCONNECTED, $oldCallFlag, $result); - $this->dispatcher->dispatchTyped($event); + if (!$result) { + // Lost the race, someone else updated the database + return; + } - return $result; + $event = new CallEndedEvent($room, $participant, $oldActiveSince); + $this->dispatcher->dispatchTyped($event); } - public function setActiveSince(Room $room, \DateTime $since, int $callFlag): bool { - $oldActiveSince = $room->getActiveSince(); + public function setActiveSince(Room $room, ?Participant $participant, \DateTime $since, int $callFlag, bool $silent): bool { $oldCallFlag = $room->getCallFlag(); + $callFlag |= $oldCallFlag; // Merge the callFlags, so events and response are with the best values if ($room->getActiveSince() instanceof \DateTime && $oldCallFlag === $callFlag) { + // Call flags of the conversation are unchanged and it's already marked active return false; } if ($room->getActiveSince() instanceof \DateTime) { - $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag); + // Call is already active, just someone upgrading the call flags + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag, $participant); $this->dispatcher->dispatchTyped($event); } else { - $event = new BeforeActiveSinceModifiedEvent($room, $since, $oldActiveSince, $callFlag, $oldCallFlag); + $details = []; + if ($silent) { + $details[AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT] = true; + } + $event = new BeforeCallStartedEvent($room, $since, $callFlag, $details, $participant); $this->dispatcher->dispatchTyped($event); } @@ -888,6 +907,7 @@ public function setActiveSince(Room $room, \DateTime $since, int $callFlag): boo $update->executeStatement(); if ($room->getActiveSince() instanceof \DateTime) { + // Call is already active, just someone upgrading the call flags $room->setActiveSince($room->getActiveSince(), $callFlag); $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag); @@ -905,10 +925,15 @@ public function setActiveSince(Room $room, \DateTime $since, int $callFlag): boo $room->setActiveSince($since, $callFlag); - $event = new ActiveSinceModifiedEvent($room, $since, $oldActiveSince, $callFlag, $oldCallFlag, $result); + if (!$result) { + // Lost the race, someone else updated the database + return false; + } + + $event = new CallStartedEvent($room, $since, $callFlag, $details, $participant); $this->dispatcher->dispatchTyped($event); - return $result; + return true; } public function setLastMessage(Room $room, IComment $message): void { diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 43e275704e1..8d3446a75d1 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -177,7 +177,7 @@ protected function notifyCallEndedForEveryone(CallEndedForEveryoneEvent $event): $this->externalSignaling->roomInCallChanged( $event->getRoom(), - $event->getNewValue(), + $event->getCallFlag(), [], true ); diff --git a/tests/integration/features/federation/call.feature b/tests/integration/features/federation/call.feature index e3b16b8f2ac..c8cc6863c1b 100644 --- a/tests/integration/features/federation/call.feature +++ b/tests/integration/features/federation/call.feature @@ -140,6 +140,48 @@ Feature: federation/call | federated_users | participant1@{$LOCAL_URL} | 0 | | users | participant2 | 0 | + Scenario: Host ends call for everyone + 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 using server "LOCAL" + And user "participant1" joins room "room" with 200 (v4) + And user "participant1" joins call "room" with 200 (v4) + | flags | 3 | + And using server "REMOTE" + And user "participant2" joins room "LOCAL::room" with 200 (v4) + And user "participant2" joins call "LOCAL::room" with 200 (v4) + | flags | 7 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 3 | + | users | participant2 | 7 | + And using server "LOCAL" + And user "participant1" ends call "room" with 200 (v4) + Then user "participant1" is participant of room "room" (v4) + | callFlag | + | 0 | + And user "participant1" sees the following attendees in room "room" with 200 (v4) + | actorType | actorId | inCall | + | users | participant1 | 0 | + | federated_users | participant2@{$REMOTE_URL} | 0 | + And using server "REMOTE" + And user "participant2" is participant of room "LOCAL::room" (v4) + | callFlag | + | 0 | + And user "participant2" sees the following attendees in room "LOCAL::room" with 200 (v4) + | actorType | actorId | inCall | + | federated_users | participant1@{$LOCAL_URL} | 0 | + | users | participant2 | 0 | + Scenario: normal call notification for federated user Given user "participant1" creates room "room" (v4) | roomType | 2 | @@ -207,6 +249,27 @@ Feature: federation/call | app | object_type | object_id | subject | | spreed | call | LOCAL::room | You missed a group call in room | + Scenario: silent call does not trigger call notification for federated users + 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) + | silent | true | + Then using server "REMOTE" + And user "participant2" has the following notifications + | app | object_type | object_id | subject | + Scenario: silent call by federated user does not trigger call notification Given user "participant1" creates room "room" (v4) | roomType | 2 |