From 438efd41baf90a131d2e931af1f23c8eb6f8b07c Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 11 May 2023 08:27:59 +0200 Subject: [PATCH] Temp Signed-off-by: Joas Schilling --- appinfo/routes.php | 1 + appinfo/routes/routesWebhookController.php | 36 +++ lib/Chat/ChatManager.php | 12 +- lib/Controller/WebhookController.php | 257 ++++++++++++++++++ lib/Events/ChatEvent.php | 3 + lib/Events/ChatParticipantEvent.php | 4 +- lib/Middleware/InjectionMiddleware.php | 1 + .../Version18000Date20230504205823.php | 3 + lib/Model/Webhook.php | 5 + lib/Service/WebhookService.php | 4 + 10 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 appinfo/routes/routesWebhookController.php create mode 100644 lib/Controller/WebhookController.php diff --git a/appinfo/routes.php b/appinfo/routes.php index b6a2a9966e2..b5142df17c1 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -45,4 +45,5 @@ include(__DIR__ . '/routes/routesSettingsController.php'), include(__DIR__ . '/routes/routesSignalingController.php'), include(__DIR__ . '/routes/routesTempAvatarController.php'), + include(__DIR__ . '/routes/routesWebhookController.php'), ); diff --git a/appinfo/routes/routesWebhookController.php b/appinfo/routes/routesWebhookController.php new file mode 100644 index 00000000000..f5c218949ad --- /dev/null +++ b/appinfo/routes/routesWebhookController.php @@ -0,0 +1,36 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +$requirements = [ + 'apiVersion' => 'v1', + 'token' => '[a-z0-9]{4,30}', +]; + +return [ + 'ocs' => [ + /** @see \OCA\Talk\Controller\WebhookController::sendMessage() */ + ['name' => 'Webhook#sendMessage', 'url' => '/api/{apiVersion}/webhook/{token}', 'verb' => 'POST', 'requirements' => $requirements], + ], +]; diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index 1de8610ecc1..f8c0613aee8 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -246,7 +246,7 @@ public function addChangelogMessage(Room $chat, string $message): IComment { * Sends a new message to the given chat. * * @param Room $chat - * @param Participant $participant + * @param ?Participant $participant * @param string $actorType * @param string $actorId * @param string $message @@ -255,7 +255,7 @@ public function addChangelogMessage(Room $chat, string $message): IComment { * @param string $referenceId * @return IComment */ - public function sendMessage(Room $chat, Participant $participant, string $actorType, string $actorId, string $message, \DateTime $creationDateTime, ?IComment $replyTo, string $referenceId, bool $silent): IComment { + public function sendMessage(Room $chat, ?Participant $participant, string $actorType, string $actorId, string $message, \DateTime $creationDateTime, ?IComment $replyTo, string $referenceId, bool $silent): IComment { $comment = $this->commentsManager->create($actorType, $actorId, 'chat', (string) $chat->getId()); $comment->setMessage($message, self::MAX_CHAT_LENGTH); $comment->setCreationDateTime($creationDateTime); @@ -273,7 +273,11 @@ public function sendMessage(Room $chat, Participant $participant, string $actorT } $this->setMessageExpiration($chat, $comment); - $event = new ChatParticipantEvent($chat, $comment, $participant, $silent); + if ($participant instanceof Participant) { + $event = new ChatParticipantEvent($chat, $comment, $participant, $silent); + } else { + $event = new ChatEvent($chat, $comment, false, $silent); + } $this->dispatcher->dispatch(self::EVENT_BEFORE_MESSAGE_SEND, $event); $shouldFlush = $this->notificationManager->defer(); @@ -281,7 +285,7 @@ public function sendMessage(Room $chat, Participant $participant, string $actorT $this->commentsManager->save($comment); // Update last_message - if ($comment->getActorType() !== 'bots' || $comment->getActorId() === 'changelog') { + if ($comment->getActorType() !== 'bots' || $comment->getActorId() === 'changelog' || str_starts_with($comment->getActorId(), 'webhook-')) { $this->roomService->setLastMessage($chat, $comment); $this->unreadCountCache->clear($chat->getId() . '-'); } else { diff --git a/lib/Controller/WebhookController.php b/lib/Controller/WebhookController.php new file mode 100644 index 00000000000..eb7e12a2f08 --- /dev/null +++ b/lib/Controller/WebhookController.php @@ -0,0 +1,257 @@ +. + * + */ + +namespace OCA\Talk\Controller; + +use OCA\Talk\Chat\AutoComplete\SearchPlugin; +use OCA\Talk\Chat\AutoComplete\Sorter; +use OCA\Talk\Chat\ChatManager; +use OCA\Talk\Chat\MessageParser; +use OCA\Talk\Chat\ReactionManager; +use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\GuestManager; +use OCA\Talk\Manager; +use OCA\Talk\MatterbridgeManager; +use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; +use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant; +use OCA\Talk\Middleware\Attribute\RequireParticipant; +use OCA\Talk\Middleware\Attribute\RequirePermission; +use OCA\Talk\Middleware\Attribute\RequireReadWriteConversation; +use OCA\Talk\Middleware\Attribute\RequireRoom; +use OCA\Talk\Model\Attachment; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Message; +use OCA\Talk\Model\Session; +use OCA\Talk\Model\Webhook; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCA\Talk\Service\AttachmentService; +use OCA\Talk\Service\AvatarService; +use OCA\Talk\Service\ChecksumVerificationService; +use OCA\Talk\Service\ParticipantService; +use OCA\Talk\Service\SessionService; +use OCA\Talk\Service\WebhookService; +use OCA\Talk\Share\RoomShareProvider; +use OCP\App\IAppManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Collaboration\AutoComplete\IManager; +use OCP\Collaboration\Collaborators\ISearchResult; +use OCP\Comments\IComment; +use OCP\Comments\MessageTooLongException; +use OCP\Comments\NotFoundException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUserManager; +use OCP\RichObjectStrings\InvalidObjectExeption; +use OCP\RichObjectStrings\IValidator; +use OCP\Security\ITrustedDomainHelper; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\User\Events\UserLiveStatusEvent; +use OCP\UserStatus\IManager as IUserStatusManager; +use OCP\UserStatus\IUserStatus; + +class WebhookController extends AEnvironmentAwareController { + private ?string $userId; + private IUserManager $userManager; + private IAppManager $appManager; + private ChatManager $chatManager; + private ReactionManager $reactionManager; + private ParticipantService $participantService; + private SessionService $sessionService; + protected AttachmentService $attachmentService; + protected avatarService $avatarService; + private GuestManager $guestManager; + /** @var string[] */ + protected array $guestNames; + private MessageParser $messageParser; + protected RoomShareProvider $shareProvider; + private IManager $autoCompleteManager; + private IUserStatusManager $statusManager; + protected MatterbridgeManager $matterbridgeManager; + private SearchPlugin $searchPlugin; + private ISearchResult $searchResult; + protected ITimeFactory $timeFactory; + protected IEventDispatcher $eventDispatcher; + protected IValidator $richObjectValidator; + protected ITrustedDomainHelper $trustedDomainHelper; + private IL10N $l; + + public function __construct( + string $appName, + IRequest $request, + ChatManager $chatManager, + ParticipantService $participantService, + AttachmentService $attachmentService, + avatarService $avatarService, + MessageParser $messageParser, + RoomShareProvider $shareProvider, + MatterbridgeManager $matterbridgeManager, + ITimeFactory $timeFactory, + IEventDispatcher $eventDispatcher, + IValidator $richObjectValidator, + ITrustedDomainHelper $trustedDomainHelper, + IL10N $l, + protected ChecksumVerificationService $checksumVerificationService, + protected WebhookService $webhookService, + protected Manager $manager, + ) { + parent::__construct($appName, $request); + $this->chatManager = $chatManager; + $this->participantService = $participantService; + $this->attachmentService = $attachmentService; + $this->avatarService = $avatarService; + $this->messageParser = $messageParser; + $this->shareProvider = $shareProvider; + $this->matterbridgeManager = $matterbridgeManager; + $this->timeFactory = $timeFactory; + $this->eventDispatcher = $eventDispatcher; + $this->richObjectValidator = $richObjectValidator; + $this->trustedDomainHelper = $trustedDomainHelper; + $this->l = $l; + } + + + public function parseCommentToResponse(IComment $comment, Message $parentMessage = null): DataResponse { + $chatMessage = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l); + $this->messageParser->parseMessage($chatMessage); + + if (!$chatMessage->getVisibility()) { + $response = new DataResponse([], Http::STATUS_CREATED); + if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) { + $response->addHeader('X-Chat-Last-Common-Read', (string) $this->chatManager->getLastCommonReadMessage($this->room)); + } + return $response; + } + + $this->participantService->updateLastReadMessage($this->participant, (int) $comment->getId()); + + $data = $chatMessage->toArray($this->getResponseFormat()); + if ($parentMessage instanceof Message) { + $data['parent'] = $parentMessage->toArray($this->getResponseFormat()); + } + + $response = new DataResponse($data, Http::STATUS_CREATED); + if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) { + $response->addHeader('X-Chat-Last-Common-Read', (string) $this->chatManager->getLastCommonReadMessage($this->room)); + } + return $response; + } + + /** + * Sends a new chat message to the given room. + * + * The author and timestamp are automatically set to the current user/guest + * and time. + * + * @param string $token conversation token + * @param string $message the message to send + * @param string $referenceId for the message to be able to later identify it again + * @param int $replyTo Parent id which this message is a reply to + * @param bool $silent If sent silent the chat message will not create any notifications + * @return DataResponse the status code is "201 Created" if successful, and + * "404 Not found" if the room or session for a guest user was not + * found". + */ + #[BruteForceProtection(action: 'webhook')] + #[PublicPage] + public function sendMessage(string $token, string $message, string $referenceId = '', int $replyTo = 0, bool $silent = false): DataResponse { + + + \OC::$server->getLogger()->error('Entry'); + $random = $this->request->getHeader('Talk-Webhook-Random'); + if (empty($random) || strlen($random) < 32) { + \OC::$server->getLogger()->error('Wrong Random'); + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + $checksum = $this->request->getHeader('Talk-Webhook-Checksum'); + if (empty($checksum)) { + \OC::$server->getLogger()->error('Wrong Checksum'); + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $webhooks = $this->webhookService->getWebhooksForToken($token); + $webhook = null; + foreach ($webhooks as $webhookAttempt) { + try { + $this->checksumVerificationService->validateRequest( + $random, + $checksum, + $webhookAttempt->getSecret(), + $message + ); + $webhook = $webhookAttempt; + break; + } catch (UnauthorizedException) { + } + } + + if (!$webhook instanceof Webhook) { + \OC::$server->getLogger()->error('No Webhook found'); + $response = new DataResponse([], Http::STATUS_UNAUTHORIZED); + $response->throttle(['action' => 'webhook']); + return $response; + } + + $room = $this->manager->getRoomByToken($token); + + $actorType = 'bots'; + $actorId = 'webhook-' . $webhook->getUrlHash(); + + $parent = $parentMessage = null; +// if ($replyTo !== 0) { +// try { +// $parent = $this->chatManager->getParentComment($this->room, (string) $replyTo); +// } catch (NotFoundException $e) { +// // Someone is trying to reply cross-rooms or to a non-existing message +// return new DataResponse([], Http::STATUS_BAD_REQUEST); +// } +// +// $parentMessage = $this->messageParser->createMessage($this->room, $this->participant, $parent, $this->l); +// $this->messageParser->parseMessage($parentMessage); +// if (!$parentMessage->isReplyable()) { +// return new DataResponse([], Http::STATUS_BAD_REQUEST); +// } +// } + +// $this->participantService->ensureOneToOneRoomIsFilled($this->room); + $creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC')); + + try { + $comment = $this->chatManager->sendMessage($room, $this->participant, $actorType, $actorId, $message, $creationDateTime, $parent, $referenceId, $silent); + } catch (MessageTooLongException) { + return new DataResponse([], Http::STATUS_REQUEST_ENTITY_TOO_LARGE); + } catch (\Exception) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse([], Http::STATUS_CREATED); + return $this->parseCommentToResponse($comment, $parentMessage); + } +} diff --git a/lib/Events/ChatEvent.php b/lib/Events/ChatEvent.php index 1b680d4810e..ff07b92b65a 100644 --- a/lib/Events/ChatEvent.php +++ b/lib/Events/ChatEvent.php @@ -30,15 +30,18 @@ class ChatEvent extends RoomEvent { protected IComment $comment; protected bool $skipLastActivityUpdate; + protected bool $silent; public function __construct( Room $room, IComment $comment, bool $skipLastActivityUpdate = false, + bool $silent = false, ) { parent::__construct($room); $this->comment = $comment; $this->skipLastActivityUpdate = $skipLastActivityUpdate; + $this->silent = $silent; } public function getComment(): IComment { diff --git a/lib/Events/ChatParticipantEvent.php b/lib/Events/ChatParticipantEvent.php index e08e0e0dba9..4519bdaf655 100644 --- a/lib/Events/ChatParticipantEvent.php +++ b/lib/Events/ChatParticipantEvent.php @@ -29,7 +29,6 @@ class ChatParticipantEvent extends ChatEvent { protected Participant $participant; - protected bool $silent; public function __construct( Room $room, @@ -37,9 +36,8 @@ public function __construct( Participant $participant, bool $silent, ) { - parent::__construct($room, $message); + parent::__construct($room, $message, false, $silent); $this->participant = $participant; - $this->silent = $silent; } public function getParticipant(): Participant { diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php index c40b929b3d3..fbb8039e6b1 100644 --- a/lib/Middleware/InjectionMiddleware.php +++ b/lib/Middleware/InjectionMiddleware.php @@ -94,6 +94,7 @@ public function beforeController(Controller $controller, string $methodName): vo if (!$controller instanceof AEnvironmentAwareController) { return; } + \OC::$server->getLogger()->error('beforeController'); $reflectionMethod = new \ReflectionMethod($controller, $methodName); diff --git a/lib/Migration/Version18000Date20230504205823.php b/lib/Migration/Version18000Date20230504205823.php index 3299b1f615d..ab933e0cd8d 100644 --- a/lib/Migration/Version18000Date20230504205823.php +++ b/lib/Migration/Version18000Date20230504205823.php @@ -54,6 +54,9 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('url', Types::STRING, [ 'length' => 4000, ]); + $table->addColumn('url_hash', Types::STRING, [ + 'length' => 64, + ]); $table->addColumn('description', Types::STRING, [ 'length' => 4000, ]); diff --git a/lib/Model/Webhook.php b/lib/Model/Webhook.php index 28ec0c3bee5..cede33f8638 100644 --- a/lib/Model/Webhook.php +++ b/lib/Model/Webhook.php @@ -30,6 +30,8 @@ * @method string getName() * @method void setUrl(string $url) * @method string getUrl() + * @method void setUrlHash(string $urlHash) + * @method string getUrlHash() * @method void setDescription(string $description) * @method string getDescription() * @method void setSecret(string $secret) @@ -40,6 +42,7 @@ class Webhook extends Entity implements \JsonSerializable { protected string $name = ''; protected string $url = ''; + protected string $urlHash = ''; protected string $description = ''; protected string $secret = ''; protected string $token = ''; @@ -47,6 +50,7 @@ class Webhook extends Entity implements \JsonSerializable { public function __construct() { $this->addType('name', 'string'); $this->addType('url', 'string'); + $this->addType('url_hash', 'string'); $this->addType('description', 'string'); $this->addType('secret', 'string'); $this->addType('token', 'string'); @@ -57,6 +61,7 @@ public function jsonSerialize(): array { 'id' => $this->getId(), 'name' => $this->getName(), 'url' => $this->getUrl(), + 'url_hash' => $this->getUrlHash(), 'description' => $this->getDescription(), 'secret' => $this->getSecret(), 'token' => $this->getToken(), diff --git a/lib/Service/WebhookService.php b/lib/Service/WebhookService.php index cfd49bdc05c..2172a2e8eb0 100644 --- a/lib/Service/WebhookService.php +++ b/lib/Service/WebhookService.php @@ -171,4 +171,8 @@ protected function getActor(Room $room): array { 'name' => $user->getDisplayName(), ]; } + + public function getWebhooksForToken(string $token): array { + return $this->webhookMapper->findForToken($token); + } }