diff options
author | Joas Schilling <coding@schilljs.com> | 2022-03-29 17:11:41 +0200 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2022-05-17 10:13:40 +0200 |
commit | c15f9d831872e6be4a8214d7bac1c0725eacd119 (patch) | |
tree | fc32c70fa3c15d618d076f45a3b892b789ed93af | |
parent | 43a4e291e278eb3580a54f0d611ee827e0163c23 (diff) |
Allow to "Send call notifications"
Signed-off-by: Joas Schilling <coding@schilljs.com>
-rw-r--r-- | appinfo/routes/routesCallController.php | 2 | ||||
-rw-r--r-- | docs/call.md | 22 | ||||
-rw-r--r-- | docs/capabilities.md | 1 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 2 | ||||
-rw-r--r-- | lib/Capabilities.php | 1 | ||||
-rw-r--r-- | lib/Controller/CallController.php | 24 | ||||
-rw-r--r-- | lib/Events/SendCallNotificationEvent.php | 46 | ||||
-rw-r--r-- | lib/Middleware/InjectionMiddleware.php | 3 | ||||
-rw-r--r-- | lib/Notification/Listener.php | 31 | ||||
-rw-r--r-- | lib/Service/ParticipantService.php | 23 | ||||
-rw-r--r-- | src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue | 39 | ||||
-rw-r--r-- | src/services/participantsService.js | 11 | ||||
-rw-r--r-- | src/store/participantsStore.js | 13 | ||||
-rw-r--r-- | tests/php/CapabilitiesTest.php | 1 |
14 files changed, 218 insertions, 1 deletions
diff --git a/appinfo/routes/routesCallController.php b/appinfo/routes/routesCallController.php index 9ea9f6018..f66a754f8 100644 --- a/appinfo/routes/routesCallController.php +++ b/appinfo/routes/routesCallController.php @@ -34,6 +34,8 @@ return [ ['name' => 'Call#getPeersForCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::joinCall() */ ['name' => 'Call#joinCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'POST', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::ringAttendee() */ + ['name' => 'Call#ringAttendee', 'url' => '/api/{apiVersion}/call/{token}/ring/{attendeeId}', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::updateCallFlags() */ ['name' => 'Call#updateCallFlags', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'PUT', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::leaveCall() */ diff --git a/docs/call.md b/docs/call.md index fba81dfe8..c92a64182 100644 --- a/docs/call.md +++ b/docs/call.md @@ -47,6 +47,28 @@ + `404 Not Found` When the user did not join the conversation before + `412 Precondition Failed` When the lobby is active and the user is not a moderator +## Send call notification + +* Required capability: `send-call-notification` +* Method: `POST` +* Endpoint: `/call/{token}/ring/{attendeeId}` +* Data: + + field | type | Description + ---|---|--- + `attendeeId` | int | The participant to notify + +* Response: + - Status code: + + `200 OK` + + `400 Bad Request` When the target participant is not a user (Guest, group, etc.) + + `400 Bad Request` When the target participant is already in the call + + `400 Bad Request` When the room has no call in process + + `400 Bad Request` When the actor is not in the call + + `403 Forbidden` When the current user is not a moderator + + `404 Not Found` When the conversation could not be found for the participant + + `412 Precondition Failed` When the lobby is active and the user is not a moderator + ## Update call flags * Method: `PUT` diff --git a/docs/capabilities.md b/docs/capabilities.md index 6208c6412..25084a2e2 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -97,4 +97,5 @@ title: Capabilities * `chat-permission` - When permission 128 is required to post chat messages, reaction or share items to the conversation * `silent-send` - Whether the chat API allows to send chat messages without triggering notifications * `sip-support-nopin` - Whether SIP can be configured to not require a custom attendee PIN +* `send-call-notification` - When the API allows to resend call notifications for individual users that did not join yet * `config => call => enabled` - Whether calling is enabled on the instance or not diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 67446c369..3d13c7c66 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -44,6 +44,7 @@ use OCA\Talk\Deck\DeckPluginLoader; use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Events\AttendeesRemovedEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Events\SendCallNotificationEvent; use OCA\Talk\Federation\CloudFederationProviderTalk; use OCA\Talk\Files\Listener as FilesListener; use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader; @@ -129,6 +130,7 @@ class Application extends App implements IBootstrap { $context->registerEventListener(RegisterOperationsEvent::class, RegisterOperationsListener::class); $context->registerEventListener(AttendeesAddedEvent::class, SystemMessageListener::class); $context->registerEventListener(AttendeesRemovedEvent::class, SystemMessageListener::class); + $context->registerEventListener(SendCallNotificationEvent::class, NotificationListener::class); $context->registerEventListener(CircleDestroyedEvent::class, CircleDeletedListener::class); $context->registerEventListener(AddingCircleMemberEvent::class, CircleMembershipListener::class); diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 18b4b2362..37f85fee2 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -104,6 +104,7 @@ class Capabilities implements IPublicCapability { 'rich-object-delete', 'chat-permission', 'silent-send', + 'send-call-notification', ], 'config' => [ 'attachments' => [ diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 6b967a0c8..26797eae5 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -137,6 +137,30 @@ class CallController extends AEnvironmentAwareController { * @PublicPage * @RequireCallEnabled * @RequireParticipant + * @RequirePermissions(permissions=call-start) + * + * @param int $attendeeId + * @return DataResponse + */ + public function ringAttendee(int $attendeeId): DataResponse { + if ($this->room->getCallFlag() === Participant::FLAG_DISCONNECTED) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + if ($this->participant->getSession() && $this->participant->getSession()->getInCall() === Participant::FLAG_DISCONNECTED) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + if (!$this->participantService->sendCallNotificationForAttendee($this->room, $this->participant, $attendeeId)) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse(); + } + + /** + * @PublicPage + * @RequireParticipant * * @param int flags * @return DataResponse diff --git a/lib/Events/SendCallNotificationEvent.php b/lib/Events/SendCallNotificationEvent.php new file mode 100644 index 000000000..8f449cb7f --- /dev/null +++ b/lib/Events/SendCallNotificationEvent.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Events; + +use OCA\Talk\Participant; +use OCA\Talk\Room; + +class SendCallNotificationEvent extends RoomEvent { + protected Participant $actor; + protected Participant $target; + + public function __construct(Room $room, Participant $actor, Participant $target) { + parent::__construct($room); + $this->actor = $actor; + $this->target = $target; + } + + public function getActor(): Participant { + return $this->actor; + } + + public function getTarget(): Participant { + return $this->target; + } +} diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php index 0671aac8c..9d63c59d5 100644 --- a/lib/Middleware/InjectionMiddleware.php +++ b/lib/Middleware/InjectionMiddleware.php @@ -209,6 +209,9 @@ class InjectionMiddleware extends Middleware { if ($textPermission === 'chat' && !($participant->getPermissions() & Attendee::PERMISSIONS_CHAT)) { throw new PermissionsException(); } + if ($textPermission === 'call-start' && !($participant->getPermissions() & Attendee::PERMISSIONS_CALL_START)) { + throw new PermissionsException(); + } } } diff --git a/lib/Notification/Listener.php b/lib/Notification/Listener.php index c4da4e2fe..5ca34f052 100644 --- a/lib/Notification/Listener.php +++ b/lib/Notification/Listener.php @@ -26,11 +26,14 @@ namespace OCA\Talk\Notification; use OCA\Talk\Events\AddParticipantsEvent; use OCA\Talk\Events\JoinRoomUserEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Events\SendCallNotificationEvent; use OCA\Talk\Model\Attendee; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; +use OCP\EventDispatcher\IEventListener; use OCP\IDBConnection; use OCP\Notification\IManager; use OCP\IUser; @@ -38,7 +41,7 @@ use OCP\IUserSession; use OCP\Server; use Psr\Log\LoggerInterface; -class Listener { +class Listener implements IEventListener { protected IDBConnection $connection; protected IManager $notificationManager; protected ParticipantService $participantsService; @@ -288,4 +291,30 @@ class Listener { return; } } + + public function handle(Event $event): void { + if ($event instanceof SendCallNotificationEvent) { + $this->sendCallNotification($event->getRoom(), $event->getActor()->getAttendee(), $event->getTarget()->getAttendee()); + } + } + + public function sendCallNotification(Room $room, Attendee $actor, Attendee $target): void { + try { + // Remove previous call notifications + $notification = $this->notificationManager->createNotification(); + $notification->setApp('spreed') + ->setObject('call', $room->getToken()) + ->setUser($target->getActorId()); + $this->notificationManager->markProcessed($notification); + + $dateTime = $this->timeFactory->getDateTime(); + $notification->setSubject('call', [ + 'callee' => $actor->getActorId(), + ]) + ->setDateTime($dateTime); + $this->notificationManager->notify($notification); + } catch (\InvalidArgumentException $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } } diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 6e181d71f..274ed2986 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -42,6 +42,7 @@ use OCA\Talk\Events\ParticipantEvent; use OCA\Talk\Events\RemoveParticipantEvent; use OCA\Talk\Events\RemoveUserEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Events\SendCallNotificationEvent; use OCA\Talk\Exceptions\ForbiddenException; use OCA\Talk\Exceptions\InvalidPasswordException; use OCA\Talk\Exceptions\ParticipantNotFoundException; @@ -990,6 +991,28 @@ class ParticipantService { } } + public function sendCallNotificationForAttendee(Room $room, Participant $currentParticipant, int $targetAttendeeId): bool { + $attendee = $this->attendeeMapper->getById($targetAttendeeId); + if ($attendee->getActorType() !== Attendee::ACTOR_USERS) { + return false; + } + + $sessions = $this->sessionMapper->findByAttendeeId($targetAttendeeId); + foreach ($sessions as $session) { + if ($session->getInCall() !== Participant::FLAG_DISCONNECTED) { + return false; + } + } + + $this->dispatcher->dispatchTyped(new SendCallNotificationEvent( + $room, + $currentParticipant, + new Participant($room, $attendee, null) + )); + + return true; + } + public function updateCallFlags(Room $room, Participant $participant, int $flags): void { $session = $participant->getSession(); if (!$session instanceof Session) { diff --git a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue index 8c5fc49c7..8987ec0b3 100644 --- a/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue +++ b/src/components/RightSidebar/Participants/ParticipantsList/Participant/Participant.vue @@ -189,6 +189,16 @@ @click="resendInvitation"> {{ t('spreed', 'Resend invitation') }} </ActionButton> + <ActionButton v-if="canSendCallNotification" + :close-after-click="true" + @click="sendCallNotification"> + <template #icon> + <Bell :size="20" + title="" + decorative /> + </template> + {{ t('spreed', 'Send call notification') }} + </ActionButton> <ActionSeparator v-if="attendeePin || canBePromoted || canBeDemoted || isEmailActor" /> <ActionButton icon="icon-delete" :close-after-click="true" @@ -228,6 +238,7 @@ import AvatarWrapper from '../../../../AvatarWrapper/AvatarWrapper' import ParticipantPermissionsEditor from './ParticipantPermissionsEditor/ParticipantPermissionsEditor.vue' // Material design icons +import Bell from 'vue-material-design-icons/Bell' import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal' import Microphone from 'vue-material-design-icons/Microphone' import Phone from 'vue-material-design-icons/Phone' @@ -253,6 +264,7 @@ export default { ParticipantPermissionsEditor, // Material design icons + Bell, DotsHorizontal, Microphone, Phone, @@ -388,6 +400,19 @@ export default { return this.participant.actorType === ATTENDEE.ACTOR_TYPE.EMAILS }, + isUserActor() { + return this.participant.actorType === ATTENDEE.ACTOR_TYPE.USERS + }, + + canSendCallNotification() { + return this.isUserActor + && !this.isSelf + && (this.currentParticipant.permissions & PARTICIPANT.PERMISSIONS.CALL_START) !== 0 + // Can also be undefined, so have to check > than disconnect + && this.currentParticipant.participantFlags > PARTICIPANT.CALL_FLAG.DISCONNECTED + && this.participant.inCall === PARTICIPANT.CALL_FLAG.DISCONNECTED + }, + computedName() { if (!this.isSearched) { const displayName = this.participant.displayName.trim() @@ -483,6 +508,7 @@ export default { currentParticipant() { return this.$store.getters.conversation(this.token) || { sessionId: '0', + participantFlags: 0, participantType: this.$store.getters.getUserId() !== null ? PARTICIPANT.TYPE.USER : PARTICIPANT.TYPE.GUEST, } }, @@ -649,6 +675,19 @@ export default { } }, + async sendCallNotification() { + try { + await this.$store.dispatch('sendCallNotification', { + token: this.token, + attendeeId: this.attendeeId, + }) + showSuccess(t('spreed', 'Notification was sent to {displayName}.', { displayName: this.participant.displayName })) + } catch (error) { + console.error(error) + showError(t('spreed', 'Could not send notification to {displayName}', { displayName: this.participant.displayName })) + } + }, + async removeParticipant() { await this.$store.dispatch('removeParticipant', { token: this.token, diff --git a/src/services/participantsService.js b/src/services/participantsService.js index 4c5e26dfd..098b68e77 100644 --- a/src/services/participantsService.js +++ b/src/services/participantsService.js @@ -164,6 +164,16 @@ const resendInvitations = async (token, { attendeeId = null }) => { } /** + * Sends call notification for the given attendee in the conversation. + * + * @param {string} token conversation token + * @param {number} attendeeId attendee id to target + */ +const sendCallNotification = async (token, { attendeeId }) => { + await axios.post(generateOcsUrl('apps/spreed/api/v4/call/{token}/ring/{attendeeId}', { token, attendeeId })) +} + +/** * Grants all permissions to an attendee in a given conversation * * @param {string} token conversation token @@ -222,6 +232,7 @@ export { fetchParticipants, setGuestUserName, resendInvitations, + sendCallNotification, grantAllPermissionsToParticipant, removeAllPermissi |