@@ -143,6 +143,86 @@ public function __construct(
143143 parent ::__construct ($ appName , $ request );
144144 }
145145
146+ /**
147+ * @return array{parent: IComment, parentMessage: Message}
148+ * @throws \InvalidArgumentException When a 400 should be thrown
149+ * @throws \DomainException When a 403 should be thrown
150+ */
151+ private function resolveReplyTo (int $ replyTo , string $ replyToToken , string $ actorType , string $ actorId , \DateTime $ creationDateTime ): array {
152+ $ isPrivateReplyFromAnotherConvo = $ replyToToken != '' ;
153+
154+ try {
155+ $ targetParentRoom = $ isPrivateReplyFromAnotherConvo ? $ this ->manager ->getRoomByToken ($ replyToToken ) : $ this ->room ;
156+ $ parent = $ this ->chatManager ->getParentComment ($ targetParentRoom , (string )$ replyTo );
157+ } catch (NotFoundException $ e ) {
158+ throw new \InvalidArgumentException ('reply-to ' , Http::STATUS_BAD_REQUEST );
159+ }
160+
161+ if ($ isPrivateReplyFromAnotherConvo ) {
162+ $ isOneToOneRoom = $ this ->room ->getType () === Room::TYPE_ONE_TO_ONE ;
163+ if (!$ isOneToOneRoom ) {
164+ throw new \InvalidArgumentException ('reply-to ' , Http::STATUS_BAD_REQUEST );
165+ }
166+
167+ $ parentActorId = $ parent ->getActorId ();
168+ $ parentActorType = $ parent ->getActorType ();
169+ // Validate if the members are part of the convo
170+ try {
171+ $ this ->participantService ->getParticipantByActor ($ targetParentRoom , $ actorType , $ actorId );
172+ $ this ->participantService ->getParticipantByActor ($ targetParentRoom , $ parentActorType , $ parentActorId );
173+ } catch (ParticipantNotFoundException $ e ) {
174+ throw new \DomainException ('reply-to ' , Http::STATUS_FORBIDDEN );
175+ }
176+
177+ $ originalParentMessage = $ this ->messageParser ->createMessage ($ targetParentRoom , $ this ->participant , $ parent , $ this ->l );
178+ $ this ->messageParser ->parseMessage ($ originalParentMessage );
179+
180+ if (!$ originalParentMessage ->isReplyable ()) {
181+ throw new \InvalidArgumentException ('reply-to ' , Http::STATUS_BAD_REQUEST );
182+ }
183+
184+ $ originalMessageData = $ originalParentMessage ->toArray ('json ' , null );
185+ $ originalMessageData ['threadId ' ] = Thread::THREAD_NONE ;
186+ $ originalMessageData ['reactions ' ] = new \stdClass ();
187+ $ originalMessageData ['messageType ' ] = $ originalParentMessage ->getMessageType ();
188+
189+ $ extraMetaData = [
190+ 'replyToMessageId ' => $ replyTo ,
191+ 'replyToConversationToken ' => $ replyToToken ,
192+ 'replyToConversationName ' => $ targetParentRoom ->getName (),
193+ 'replyToActorDisplayName ' => $ originalParentMessage ->getActorDisplayName (),
194+ ];
195+ $ originalMessageData ['metaData ' ] = array_merge ($ originalMessageData ['metaData ' ] ?? [], $ extraMetaData );
196+ // Create a link message for the private reply
197+ $ copiedParent = $ this ->chatManager ->sendMessage (
198+ $ this ->room ,
199+ $ this ->participant ,
200+ $ parentActorType ,
201+ $ parentActorId ,
202+ ChatManager::VERB_PRIVATE_REPLY ,
203+ $ creationDateTime ,
204+ null ,
205+ '' ,
206+ true ,
207+ verb: ChatManager::VERB_PRIVATE_REPLY ,
208+ extraMetaData: [
209+ 'originalMessage ' => $ originalMessageData ,
210+ ],
211+ );
212+
213+ $ parentMessage = $ this ->messageParser ->createMessage ($ this ->room , $ this ->participant , $ copiedParent , $ this ->l );
214+ $ this ->messageParser ->parseMessage ($ parentMessage );
215+ } else {
216+ $ parentMessage = $ this ->messageParser ->createMessage ($ targetParentRoom , $ this ->participant , $ parent , $ this ->l );
217+ $ this ->messageParser ->parseMessage ($ parentMessage );
218+
219+ if (!$ parentMessage ->isReplyable ()) {
220+ throw new \InvalidArgumentException ('reply-to ' , Http::STATUS_BAD_REQUEST );
221+ }
222+ }
223+
224+ return ['parent ' => $ isPrivateReplyFromAnotherConvo ? $ copiedParent : $ parent , 'parentMessage ' => $ parentMessage ];
225+ }
146226 /**
147227 * @return list{0: Attendee::ACTOR_*, 1: string}
148228 */
@@ -219,7 +299,12 @@ protected function parseCommentToResponse(IComment $comment, ?Message $parentMes
219299 }
220300 $ data = $ chatMessage ->toArray ($ this ->getResponseFormat (), $ thread );
221301 if ($ parentMessage instanceof Message) {
222- $ data ['parent ' ] = $ parentMessage ->toArray ($ this ->getResponseFormat (), $ thread );
302+ $ parentComment = $ parentMessage ->getComment ();
303+ if ($ parentComment ->getVerb () === ChatManager::VERB_PRIVATE_REPLY && $ parentComment ->getParentId () === '0 ' ) {
304+ $ data ['parent ' ] = $ this ->buildPrivateReplyParentSnapshot ($ parentComment );
305+ } else {
306+ $ data ['parent ' ] = $ parentMessage ->toArray ($ this ->getResponseFormat (), $ thread );
307+ }
223308 }
224309
225310 $ headers = [];
@@ -240,13 +325,15 @@ protected function parseCommentToResponse(IComment $comment, ?Message $parentMes
240325 * @param string $referenceId for the message to be able to later identify it again
241326 * @param int $replyTo Parent id which this message is a reply to
242327 * @psalm-param non-negative-int $replyTo
328+ * @param string $replyToToken Parent token to which reply is initiated
243329 * @param bool $silent If sent silent the chat message will not create any notifications
244330 * @param string $threadTitle Only supported when not replying, when given will create a thread (requires `threads` capability)
245331 * @param int $threadId Thread id which this message is a reply to without quoting a specific message (ignored when $replyTo is given, also requires `threads` capability)
246- * @return DataResponse<Http::STATUS_CREATED, ?TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_REQUEST_ENTITY_TOO_LARGE|Http::STATUS_TOO_MANY_REQUESTS, array{error: string}, array{}>
332+ * @return DataResponse<Http::STATUS_CREATED, ?TalkChatMessageWithParent, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http:: STATUS_NOT_FOUND|Http::STATUS_REQUEST_ENTITY_TOO_LARGE|Http::STATUS_TOO_MANY_REQUESTS, array{error: string}, array{}>
247333 *
248334 * 201: Message sent successfully
249335 * 400: Sending message is not possible
336+ * 403: When trying to cross reference wrongly on a reply-private
250337 * 404: Actor not found
251338 * 413: Message too long
252339 * 429: Mention rate limit exceeded (guests only)
@@ -262,7 +349,7 @@ protected function parseCommentToResponse(IComment $comment, ?Message $parentMes
262349 'apiVersion ' => '(v1) ' ,
263350 'token ' => '[a-z0-9]{4,30} ' ,
264351 ])]
265- public function sendMessage (string $ message , string $ actorDisplayName = '' , string $ referenceId = '' , int $ replyTo = 0 , bool $ silent = false , string $ threadTitle = '' , int $ threadId = 0 ): DataResponse {
352+ public function sendMessage (string $ message , string $ actorDisplayName = '' , string $ referenceId = '' , int $ replyTo = 0 , string $ replyToToken = '' , bool $ silent = false , string $ threadTitle = '' , int $ threadId = 0 ): DataResponse {
266353 if ($ this ->room ->isFederatedConversation ()) {
267354 /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */
268355 $ proxy = \OCP \Server::get (\OCA \Talk \Federation \Proxy \TalkV1 \Controller \ChatController::class);
@@ -279,27 +366,25 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri
279366 }
280367
281368 $ parent = $ parentMessage = null ;
369+ $ creationDateTime = $ this ->timeFactory ->getDateTime ('now ' , new \DateTimeZone ('UTC ' ));
370+
282371 if ($ replyTo !== 0 ) {
283372 try {
284- $ parent = $ this ->chatManager ->getParentComment ($ this ->room , (string )$ replyTo );
285- } catch (NotFoundException $ e ) {
286- // Someone is trying to reply cross-rooms or to a non-existing message
287- return new DataResponse (['error ' => 'reply-to ' ], Http::STATUS_BAD_REQUEST );
288- }
289-
290- $ parentMessage = $ this ->messageParser ->createMessage ($ this ->room , $ this ->participant , $ parent , $ this ->l );
291- $ this ->messageParser ->parseMessage ($ parentMessage );
292- if (!$ parentMessage ->isReplyable ()) {
373+ $ resolvedReplyTo = $ this ->resolveReplyTo ($ replyTo , $ replyToToken , $ actorType , $ actorId , $ creationDateTime );
374+ } catch (\InvalidArgumentException ) {
293375 return new DataResponse (['error ' => 'reply-to ' ], Http::STATUS_BAD_REQUEST );
376+ } catch (\DomainException ) {
377+ return new DataResponse (['error ' => 'reply-to ' ], Http::STATUS_FORBIDDEN );
294378 }
379+ $ parent = $ resolvedReplyTo ['parent ' ];
380+ $ parentMessage = $ resolvedReplyTo ['parentMessage ' ];
295381 } elseif ($ threadId !== 0 ) {
296382 if (!$ this ->threadService ->validateThread ($ this ->room ->getId (), $ threadId )) {
297383 return new DataResponse (['error ' => 'reply-to ' ], Http::STATUS_BAD_REQUEST );
298384 }
299385 }
300386
301387 $ this ->participantService ->ensureOneToOneRoomIsFilled ($ this ->room );
302- $ creationDateTime = $ this ->timeFactory ->getDateTime ('now ' , new \DateTimeZone ('UTC ' ));
303388
304389 try {
305390 $ createThread = $ replyTo === 0 && $ threadId === Thread::THREAD_NONE && $ threadTitle !== '' ;
@@ -398,10 +483,11 @@ public function getScheduledMessages(): DataResponse {
398483 * @param bool $silent If sent silent the scheduled message will not create any notifications when sent
399484 * @param string $threadTitle Only supported when not replying, when given will create a thread (requires `threads` capability)
400485 * @param int $threadId Thread id without quoting a specific message (requires `threads` capability)
401- * @return DataResponse<Http::STATUS_CREATED, TalkScheduledMessage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'message'|'reply-to'|'send-at'}, array{}>|DataResponse<Http::STATUS_REQUEST_ENTITY_TOO_LARGE, array{error: 'message'}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'actor'}, array{}>
486+ * @return DataResponse<Http::STATUS_CREATED, TalkScheduledMessage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'message'|'reply-to'|'send-at'}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: 'reply-to'}, array{}>|DataResponse<Http:: STATUS_REQUEST_ENTITY_TOO_LARGE, array{error: 'message'}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'actor'}, array{}>
402487 *
403488 * 201: Message scheduled successfully
404489 * 400: Scheduling the message is not possible
490+ * 403: When trying to cross reference wrongly on a reply-private
405491 * 404: Actor not found
406492 * 413: Message too long
407493 */
@@ -435,19 +521,19 @@ public function scheduleMessage(
435521 }
436522
437523 $ parent = $ parentMessage = null ;
524+ $ creationDateTime = $ this ->timeFactory ->getDateTime ('now ' , new \DateTimeZone ('UTC ' ));
525+
438526 if ($ replyTo !== 0 ) {
527+ [$ actorType , $ actorId ] = $ this ->getActorInfo ();
439528 try {
440- $ parent = $ this ->chatManager ->getParentComment ($ this ->room , (string )$ replyTo );
441- } catch (NotFoundException $ e ) {
442- // Someone is trying to reply cross-rooms or to a non-existing message
443- return new DataResponse (['error ' => 'reply-to ' ], Http::STATUS_BAD_REQUEST );
444- }
445-
446- $ parentMessage = $ this ->messageParser ->createMessage ($ this ->room , $ this ->participant , $ parent , $ this ->l );
447- $ this ->messageParser ->parseMessage ($ parentMessage );
448- if (!$ parentMessage ->isReplyable ()) {
529+ $ resolvedReplyTo = $ this ->resolveReplyTo ($ replyTo , '' , $ actorType , $ actorId , $ creationDateTime );
530+ } catch (\InvalidArgumentException ) {
449531 return new DataResponse (['error ' => 'reply-to ' ], Http::STATUS_BAD_REQUEST );
532+ } catch (\DomainException ) {
533+ return new DataResponse (['error ' => 'reply-to ' ], Http::STATUS_FORBIDDEN );
450534 }
535+ $ parent = $ resolvedReplyTo ['parent ' ];
536+ $ parentMessage = $ resolvedReplyTo ['parentMessage ' ];
451537 }
452538
453539 if ($ threadId !== 0 && !$ this ->threadService ->validateThread ($ this ->room ->getId (), $ threadId )) {
@@ -1069,6 +1155,12 @@ protected function prepareCommentsAsDataResponse(array $comments, int $lastCommo
10691155 continue ;
10701156 }
10711157
1158+ if ($ comment ->getVerb () === ChatManager::VERB_PRIVATE_REPLY && $ comment ->getParentId () === '0 ' ) {
1159+ $ loadedParents [$ parentId ] = $ this ->buildPrivateReplyParentSnapshot ($ comment );
1160+ $ messages [$ commentKey ]['parent ' ] = $ loadedParents [$ parentId ];
1161+ continue ;
1162+ }
1163+
10721164 $ expireDate = $ message ->getComment ()->getExpireDate ();
10731165 if ($ expireDate instanceof \DateTime && $ expireDate < $ now ) {
10741166 $ commentIdToIndex [$ id ] = null ;
@@ -1671,6 +1763,16 @@ public function getUpcomingReminders(): DataResponse {
16711763 return new DataResponse ($ resultData , Http::STATUS_OK );
16721764 }
16731765
1766+ private function buildPrivateReplyParentSnapshot (IComment $ comment ): array {
1767+ $ metaData = $ comment ->getMetaData () ?? [];
1768+ $ message = $ metaData ['originalMessage ' ];
1769+ $ message ['id ' ] = (int )$ comment ->getId ();
1770+ unset($ metaData ['originalMessage ' ]);
1771+ $ message ['metaData ' ] = array_merge ($ metaData , $ message ['metaData ' ] ?? []);
1772+
1773+ return $ message ;
1774+ }
1775+
16741776 /**
16751777 * @throws DoesNotExistException
16761778 * @throws CannotReachRemoteException
0 commit comments