From 48b2c8c04e3037e6ebaaade30ed9b0691e2e3c66 Mon Sep 17 00:00:00 2001 From: Eliseu Amaro Date: Sun, 19 Dec 2021 17:43:43 +0000 Subject: [PATCH] [COMPONENTS][Conversation] Local Conversations done [COMPONENTS][Posting] Call Conversation::assignLocalConversation upon creating a new note By using the AddExtraArgsToNoteContent event upon posting a Note, an extra argument ('reply_to') is added before storing the aforementioned Note. When storeLocalNote eventually creates the Note, the corresponding Conversation is assigned. --- .../Conversation/Controller/Conversation.php | 47 +++----- components/Conversation/Controller/Reply.php | 110 +++--------------- components/Conversation/Conversation.php | 77 ++++++++---- .../Conversation/Entity/Conversation.php | 11 +- .../templates/reply/add_reply.html.twig | 1 - components/Posting/Posting.php | 35 +++++- components/Tag/Tag.php | 2 +- 7 files changed, 123 insertions(+), 160 deletions(-) diff --git a/components/Conversation/Controller/Conversation.php b/components/Conversation/Controller/Conversation.php index 00c7e8e3af..4a49824ec3 100644 --- a/components/Conversation/Controller/Conversation.php +++ b/components/Conversation/Controller/Conversation.php @@ -26,46 +26,31 @@ declare(strict_types = 1); namespace Component\Conversation\Controller; -use _PHPStan_76800bfb5\Nette\NotImplementedException; use App\Core\Controller\FeedController; use App\Core\DB\DB; -use App\Core\Form; -use App\Util\Exception\DuplicateFoundException; -use App\Util\Exception\NoLoggedInUser; -use App\Util\Exception\ServerException; -use function App\Core\I18n\_m; -use App\Core\Log; -use App\Core\Router\Router; -use App\Entity\Actor; -use App\Entity\Note; -use App\Util\Common; -use App\Util\Exception\ClientException; -use App\Util\Exception\InvalidFormException; -use App\Util\Exception\NoSuchNoteException; -use App\Util\Exception\RedirectException; -use App\Util\Form\FormFields; -use Component\Posting\Posting; -use Symfony\Component\Form\Extension\Core\Type\FileType; -use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\HttpFoundation\Request; class Conversation extends FeedController { - // if note is root -> just link - // if note is a reply -> link from above plus anchor - public function ConversationShow(Request $request) + /** + * Render conversation page + * + * @return array + */ + public function showConversation(Request $request, int $conversation_id) { - throw new NotImplementedException(); - $actor_id = Common::ensureLoggedIn()->getId(); - $notes = DB::dql('select n from App\Entity\Note n ' - . 'where n.reply_to is not null and n.actor_id = :id ' - . 'order by n.created DESC', ['id' => $actor_id], ); + // TODO: + // if note is root -> just link + // if note is a reply -> link from above plus anchor + + $notes = DB::dql('select n from App\Entity\Note n ' + . 'on n.conversation_id = :id ' + . 'order by n.created DESC', ['id' => $conversation_id], ); return [ - '_template' => 'feeds/feed.html.twig', - 'notes' => $notes, + '_template' => 'feeds/feed.html.twig', + 'notes' => $notes, 'should_format' => false, - 'page_title' => 'Replies feed', + 'page_title' => 'Conversation', ]; } } diff --git a/components/Conversation/Controller/Reply.php b/components/Conversation/Controller/Reply.php index 36aecdca05..60c4713401 100644 --- a/components/Conversation/Controller/Reply.php +++ b/components/Conversation/Controller/Reply.php @@ -28,25 +28,12 @@ namespace Component\Conversation\Controller; use App\Core\Controller\FeedController; use App\Core\DB\DB; -use App\Core\Form; -use function App\Core\I18n\_m; -use App\Core\Log; -use App\Core\Router\Router; -use App\Entity\Actor; use App\Entity\Note; use App\Util\Common; use App\Util\Exception\ClientException; -use App\Util\Exception\DuplicateFoundException; -use App\Util\Exception\InvalidFormException; use App\Util\Exception\NoLoggedInUser; use App\Util\Exception\NoSuchNoteException; -use App\Util\Exception\RedirectException; use App\Util\Exception\ServerException; -use App\Util\Form\FormFields; -use Component\Posting\Posting; -use Symfony\Component\Form\Extension\Core\Type\FileType; -use Symfony\Component\Form\Extension\Core\Type\SubmitType; -use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\HttpFoundation\Request; class Reply extends FeedController @@ -55,110 +42,45 @@ class Reply extends FeedController * Controller for the note reply non-JS page * * @throws ClientException - * @throws DuplicateFoundException - * @throws InvalidFormException * @throws NoLoggedInUser * @throws NoSuchNoteException - * @throws RedirectException * @throws ServerException * * @return array */ - public function replyAddNote(Request $request, int $id) + public function addReply(Request $request, int $note_id, int $actor_id) { - $user = Common::ensureLoggedIn(); - $actor_id = $user->getId(); + $user = Common::ensureLoggedIn(); - $note = Note::getByPK($id); + $note = Note::getByPK($note_id); if (\is_null($note) || !$note->isVisibleTo($user)) { throw new NoSuchNoteException(); } - /* - * TODO shouldn't this be the posting form? - * Posting needs to be improved to do that. Currently, if it was used here, - * there are only slow ways to retrieve the resulting note. - * Not only is it part of a right panel event, there's an immediate redirect exception - * after submitting it. - * That event needs to be extended to allow this component to automagically fill the To: field and get the - * resulting note - */ - $form = Form::create([ - ['content', TextareaType::class, ['label' => _m('Reply'), 'label_attr' => ['class' => 'section-form-label'], 'help' => _m('Please input your reply.')]], - FormFields::language( - $user->getActor(), - context_actor: $note->getActor(), - label: _m('Note language'), - ), - ['attachments', FileType::class, ['label' => ' ', 'multiple' => true, 'required' => false]], - ['replyform', SubmitType::class, ['label' => _m('Submit')]], - ]); - - $form->handleRequest($request); - - if ($form->isSubmitted()) { - $data = $form->getData(); - if ($form->isValid()) { - // Create a new note with the same content as the original - $reply = Posting::storeLocalNote( - actor: Actor::getByPK($actor_id), - content: $data['content'], - content_type: 'text/plain', // TODO - language: $data['language'], - attachments: $data['attachments'], - ); - - // Update DB - DB::persist($reply); - DB::flush(); - - // Find the id of the note we just created - $reply_id = $reply->getId(); - $parent_id = $note->getId(); - $resulting_note = Note::getByPK($reply_id); - $resulting_note->setReplyTo($parent_id); - - // Update DB one last time - DB::merge($note); - DB::flush(); - - // Redirect user to where they came from - // Prevent open redirect - if (!\is_null($from = $this->string('from'))) { - if (Router::isAbsolute($from)) { - Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$from})"); - throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive) - } else { - // TODO anchor on element id - throw new RedirectException($from); - } - } else { - // If we don't have a URL to return to, go to the instance root - throw new RedirectException('root'); - } - } else { - throw new InvalidFormException(); - } - } - return [ '_template' => 'reply/add_reply.html.twig', 'note' => $note, - 'add_reply' => $form->createView(), ]; } - public function replies(Request $request) + /** + * Render actor replies page + * + * @throws NoLoggedInUser + * + * @return array + */ + public function showReplies(Request $request) { $actor_id = Common::ensureLoggedIn()->getId(); $notes = DB::dql('select n from App\Entity\Note n ' - . 'where n.reply_to is not null and n.actor_id = :id ' - . 'order by n.created DESC', ['id' => $actor_id], ); + . 'where n.reply_to is not null and n.actor_id = :id ' + . 'order by n.created DESC', ['id' => $actor_id], ); return [ - '_template' => 'feeds/feed.html.twig', - 'notes' => $notes, + '_template' => 'feeds/feed.html.twig', + 'notes' => $notes, 'should_format' => false, - 'page_title' => 'Replies feed', + 'page_title' => 'Replies feed', ]; } } diff --git a/components/Conversation/Conversation.php b/components/Conversation/Conversation.php index a9167bdaf7..9705c8af8f 100644 --- a/components/Conversation/Conversation.php +++ b/components/Conversation/Conversation.php @@ -38,10 +38,49 @@ use App\Util\Exception\ServerException; use App\Util\Formatting; use App\Util\Nickname; use Component\Conversation\Controller\Reply as ReplyController; +use Component\Conversation\Entity\Conversation as ConversationEntity; +use const SORT_REGULAR; use Symfony\Component\HttpFoundation\Request; class Conversation extends Component { + /** + * **Assigns** the given local Note it's corresponding **Conversation** + * + * **If a _$parent_id_ is not given**, then the Actor is not attempting a reply, + * therefore, we can assume (for now) that we need to create a new Conversation and assign it + * to the newly created Note (please look at Component\Posting::storeLocalNote, where this function is used) + * + * **On the other hand**, given a _$parent_id_, the Actor is attempting to post a reply. Meaning that, + * this Note conversation_id should be same as the parent Note + */ + public static function assignLocalConversation(Note $current_note, ?int $parent_id): void + { + if (!$parent_id) { + // If none found, we don't know yet if it is a reply or root + // Let's assume for now that it's a new conversation and deal with stitching later + $conversation = ConversationEntity::create(['initial_note_id' => $current_note->getId()]); + + // We need the Conversation id itself, so a persist is in order + DB::persist($conversation); + + // Set the Uri and current_note's own conversation_id + $conversation->setUri(Router::url('conversation', ['conversation_id' => $conversation->getId()], Router::ABSOLUTE_URL)); + $current_note->setConversationId($conversation->getId()); + } else { + // It's a reply for sure + // Set reply_to property in newly created Note to parent's id + $current_note->setReplyTo($parent_id); + + // Parent will have a conversation of its own, the reply should have the same one + $parent_note = Note::getById($parent_id); + $current_note->setConversationId($parent_note->getConversationId()); + } + + DB::merge($current_note); + DB::flush(); + } + /** * HTML rendering event that adds a reply link as a note * action, if a user is logged in @@ -52,14 +91,15 @@ class Conversation extends Component return Event::next; } - // Generating URL for repeat action route - $args = ['id' => $note->getId()]; + // Generating URL for reply action route + $args = ['note_id' => $note->getId(), 'actor_id' => $note->getActor()->getId()]; $type = Router::ABSOLUTE_PATH; $reply_action_url = Router::url('reply_add', $args, $type); $query_string = $request->getQueryString(); // Concatenating get parameter to redirect the user to where he came from - $reply_action_url .= !\is_null($query_string) ? '?from=' . mb_substr($query_string, 2) : ''; + $reply_action_url .= !\is_null($query_string) ? '?from=' : '&from='; + $reply_action_url .= mb_substr($query_string, 2); $reply_action = [ 'url' => $reply_action_url, @@ -72,6 +112,14 @@ class Conversation extends Component return Event::next; } + public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args): bool + { + // If Actor is adding a reply, get parent's Note id + // Else it's null + $extra_args['reply_to'] = $request->get('_route') === 'reply_add' ? (int) $request->get('note_id') : null; + return Event::next; + } + /** * Append on note information about user actions */ @@ -95,7 +143,7 @@ class Conversation extends Component } // Filter out multiple replies from the same actor - $reply_actor = array_unique($reply_actor, \SORT_REGULAR); + $reply_actor = array_unique($reply_actor, SORT_REGULAR); // Add to complementary info foreach ($reply_actor as $actor) { @@ -125,27 +173,16 @@ class Conversation extends Component return Event::next; } - public function onProcessNoteContent(Note $note, string $content): bool + public function onAddRoute(RouteLoader $r): bool { - // If the source lacks capability of sending the "reply_to" - // metadata, let's try to find an inline reply_to-reference. - // TODO: preg match any reply_to reference and handle reply to funky business (see Link component) - return Event::next; - } - - /** - * @return bool - */ - public function onAddRoute(RouteLoader $r) - { - $r->connect('reply_add', '/object/note/{id<\d+>}/reply', [ReplyController::class, 'replyAddNote']); - $r->connect('replies', '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/replies', [ReplyController::class, 'replies']); - $r->connect('conversation', '/conversation/{id<\d+>}', [ReplyController::class, 'conversation']); + $r->connect('reply_add', '/object/note/new?to={actor_id<\d+>}&reply_to={note_id<\d+>}', [ReplyController::class, 'addReply']); + $r->connect('replies', '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/replies', [ReplyController::class, 'showReplies']); + $r->connect('conversation', '/conversation/{conversation_id<\d+>}', [Controller\Conversation::class, 'showConversation']); return Event::next; } - public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering) + public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): bool { DB::persist(Feed::create([ 'actor_id' => $actor_id, diff --git a/components/Conversation/Entity/Conversation.php b/components/Conversation/Entity/Conversation.php index 4e432ef79c..fb34304cb8 100644 --- a/components/Conversation/Entity/Conversation.php +++ b/components/Conversation/Entity/Conversation.php @@ -23,9 +23,7 @@ declare(strict_types = 1); namespace Component\Conversation\Entity; -use App\Core\DB\DB; use App\Core\Entity; -use App\Entity\Note; /** * Entity class for Conversations @@ -83,7 +81,6 @@ class Conversation extends Entity return $this->initial_note_id; } - // @codeCoverageIgnoreEnd // }}} Autocode @@ -92,11 +89,11 @@ class Conversation extends Entity return [ 'name' => 'conversation', 'fields' => [ - 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Serial identifier, since any additional meaning would require updating its value for every reply upon receiving a new aparent root'], - 'uri' => ['type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'URI of the conversation'], - 'initial_note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Initial note seen on this host for this conversation'], + 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Serial identifier, since any additional meaning would require updating its value for every reply upon receiving a new aparent root'], + 'uri' => ['type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'URI of the conversation'], + 'initial_note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Initial note seen on this host for this conversation'], ], - 'primary key' => ['id'], + 'primary key' => ['id'], 'unique keys' => [ 'conversation_uri_uniq' => ['uri'], ], diff --git a/components/Conversation/templates/reply/add_reply.html.twig b/components/Conversation/templates/reply/add_reply.html.twig index 4121eed9e5..293b3da428 100644 --- a/components/Conversation/templates/reply/add_reply.html.twig +++ b/components/Conversation/templates/reply/add_reply.html.twig @@ -13,7 +13,6 @@
{{ noteView.macro_note_minimal(note) }} - {{ form(add_reply) }}
{% endblock body %} diff --git a/components/Posting/Posting.php b/components/Posting/Posting.php index d2f68e48fc..351d29ebba 100644 --- a/components/Posting/Posting.php +++ b/components/Posting/Posting.php @@ -25,6 +25,7 @@ namespace Component\Posting; use App\Core\Cache; use App\Core\DB\DB; +use App\Core\Entity; use App\Core\Event; use App\Core\Form; use App\Core\GSFile; @@ -39,6 +40,7 @@ use App\Entity\Language; use App\Entity\Note; use App\Util\Common; use App\Util\Exception\ClientException; +use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\RedirectException; use App\Util\Exception\ServerException; use App\Util\Form\FormFields; @@ -46,6 +48,7 @@ use App\Util\Formatting; use Component\Attachment\Entity\ActorToAttachment; use Component\Attachment\Entity\Attachment; use Component\Attachment\Entity\AttachmentToNote; +use Component\Conversation\Conversation; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -67,7 +70,7 @@ class Posting extends Component */ public function onAppendRightPostingBlock(Request $request, array &$res): bool { - if (($user = Common::user()) === null) { + if (\is_null($user = Common::user())) { return Event::next; } @@ -95,8 +98,22 @@ class Posting extends Component ]; Event::handle('PostingAvailableContentTypes', [&$available_content_types]); - $context_actor = null; // This is where we'd plug in the group in which the actor is posting, or whom they're replying to - $form_params = [ + // TODO: this needs work + // This is where we'd plug in the group in which the actor is posting, or whom they're replying to + // store local note needs to know what conversation it is + // Conversation adds the respective query string on route url, for groups it should be handled by an event + $to_query = $request->get('actor_id'); + $context_actor = null; + + // Actor is posting in a group? + if (!\is_null($to_query)) { + // Getting the actor itself + $context_actor = Actor::getById((int) $to_query); + // Adding it to the to_tags array TODO: this is wrong + $to_tags[] = $context_actor->getNickname(); + } + + $form_params = [ ['to', ChoiceType::class, ['label' => _m('To:'), 'multiple' => false, 'expanded' => false, 'choices' => $to_tags]], ['visibility', ChoiceType::class, ['label' => _m('Visibility:'), 'multiple' => false, 'expanded' => false, 'data' => 'public', 'choices' => [_m('Public') => 'public', _m('Instance') => 'instance', _m('Private') => 'private']]], ['content', TextareaType::class, ['label' => _m('Content:'), 'data' => $initial_content, 'attr' => ['placeholder' => _m($placeholder)], 'constraints' => [new Length(['max' => Common::config('site', 'text_limit')])]]], @@ -131,7 +148,7 @@ class Posting extends Component $content_type = $data['content_type'] ?? $available_content_types[array_key_first($available_content_types)]; $extra_args = []; - Event::handle('PostingHandleForm', [$request, $actor, $data, &$extra_args, $form_params, $form]); + Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form_params, $form]); self::storeLocalNote( $user->getActor(), @@ -141,6 +158,7 @@ class Posting extends Component $data['attachments'], process_note_content_extra_args: $extra_args, ); + throw new RedirectException(); } } catch (FormSizeFileException $sizeFileException) { @@ -162,11 +180,11 @@ class Posting extends Component * @param array $processed_attachments Array of [Attachment, Attachment's name] to be associated to this $actor and Note * @param array $process_note_content_extra_args Extra arguments for the event ProcessNoteContent * - * @throws \App\Util\Exception\DuplicateFoundException * @throws ClientException + * @throws DuplicateFoundException * @throws ServerException * - * @return \App\Core\Entity|mixed + * @return Entity|mixed */ public static function storeLocalNote( Actor $actor, @@ -212,6 +230,11 @@ class Posting extends Component Event::handle('ProcessNoteContent', [$note, $content, $content_type, $process_note_content_extra_args]); } + // Assign conversation to this note + // AddExtraArgsToNoteContent already added the info we need + $reply_to = $process_note_content_extra_args['reply_to']; + Conversation::assignLocalConversation($note, $reply_to); + if ($processed_attachments !== []) { foreach ($processed_attachments as [$a, $fname]) { if (DB::count('actor_to_attachment', $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor->getId()]) === 0) { diff --git a/components/Tag/Tag.php b/components/Tag/Tag.php index aa2278dc0c..d24aa2429a 100644 --- a/components/Tag/Tag.php +++ b/components/Tag/Tag.php @@ -184,7 +184,7 @@ class Tag extends Component return Event::next; } - public function onPostingHandleForm(Request $request, Actor $actor, array $data, array &$extra_args) + public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args) { if (!isset($data['tag_use_canonical'])) { throw new ClientException;