diff --git a/plugins/Repeat/Repeat.php b/plugins/Repeat/Repeat.php deleted file mode 100644 index c702764a26..0000000000 --- a/plugins/Repeat/Repeat.php +++ /dev/null @@ -1,161 +0,0 @@ -. -// }}} - -namespace Plugin\Repeat; - -use App\Core\DB\DB; -use App\Core\Event; -use function App\Core\I18n\_m; -use App\Core\Modules\NoteHandlerPlugin; -use App\Core\Router\RouteLoader; -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\NoSuchNoteException; -use App\Util\Exception\NotFoundException; -use App\Util\Exception\RedirectException; -use App\Util\Exception\ServerException; -use App\Util\Formatting; -use Plugin\Repeat\Entity\NoteRepeat; -use Symfony\Component\HttpFoundation\Request; - -class Repeat extends NoteHandlerPlugin -{ - /** - * HTML rendering event that adds the repeat form as a note - * action, if a user is logged in - * - * @throws InvalidFormException - * @throws NoSuchNoteException - * @throws RedirectException*@throws ClientException*@throws DuplicateFoundException - * - * @return bool Event hook - */ - public function onAddNoteActions(Request $request, Note $note, array &$actions): bool - { - if (\is_null($user = Common::user())) { - return Event::next; - } - - // If note is repeated, "is_repeated" is 1 - $is_repeat = DB::count('note_repeat', ['note_id' => $note->getId()]) >= 1; - - try { - if (DB::findOneBy('note_repeat', ['repeat_of' => $note->getId()])) { - return Event::next; - } - } catch (DuplicateFoundException|NotFoundException $e) { - } - - // Generating URL for repeat action route - $args = ['id' => $note->getId()]; - $type = Router::ABSOLUTE_PATH; - $repeat_action_url = $is_repeat - ? Router::url('repeat_remove', $args, $type) - : Router::url('repeat_add', $args, $type); - - // TODO clean this up - // SECURITY: open redirect? - $query_string = $request->getQueryString(); - // Concatenating get parameter to redirect the user to where he came from - $repeat_action_url .= !\is_null($query_string) ? '?from=' . mb_substr($query_string, 2) : ''; - - $extra_classes = $is_repeat ? 'note-actions-set' : 'note-actions-unset'; - $repeat_action = [ - 'url' => $repeat_action_url, - 'title' => $is_repeat ? 'Remove this repeat' : 'Repeat this note!', - 'classes' => "button-container repeat-button-container {$extra_classes}", - 'id' => 'repeat-button-container-' . $note->getId(), - ]; - - $actions[] = $repeat_action; - return Event::next; - } - - /** - * Append on note information about user actions. - * - * @return array|bool - */ - public function onAppendCardNote(array $vars, array &$result) - { - // if note is the original and user isn't the one who repeated, append on end "user repeated this" - // if user is the one who repeated, append on end "you repeated this, remove repeat?" - $check_user = !\is_null(Common::user()); - - $note = $vars['note']; - - $complementary_info = ''; - $repeat_actor = []; - $note_repeats = NoteRepeat::getNoteRepeats($note); - - // Get actors who replied - foreach ($note_repeats as $reply) { - $repeat_actor[] = Actor::getWithPK($reply->getActorId()); - } - if (\count($repeat_actor) < 1) { - return Event::next; - } - - // Filter out multiple replies from the same actor - $repeat_actor = array_unique($repeat_actor, \SORT_REGULAR); - - // Add to complementary info - foreach ($repeat_actor as $actor) { - $repeat_actor_url = $actor->getUrl(); - $repeat_actor_nickname = $actor->getNickname(); - - if ($check_user && $actor->getId() === (Common::actor())->getId()) { - // If the repeat is yours - try { - $you_translation = _m('You'); - } catch (ServerException $e) { - $you_translation = 'You'; - } - - $prepend = "{$you_translation}, " . ($prepend = &$complementary_info); - $complementary_info = $prepend; - } else { - // If the repeat is from someone else - $complementary_info .= "{$repeat_actor_nickname}, "; - } - } - - $complementary_info = rtrim(trim($complementary_info), ','); - $complementary_info .= ' repeated this note.'; - $result[] = Formatting::twigRenderString($complementary_info, []); - - return $result; - } - - public function onAddRoute(RouteLoader $r): bool - { - // Add/remove note to/from repeats - $r->connect(id: 'repeat_add', uri_path: '/object/note/{id<\d+>}/repeat', target: [Controller\Repeat::class, 'repeatAddNote']); - $r->connect(id: 'repeat_remove', uri_path: '/object/note/{id<\d+>}/unrepeat', target: [Controller\Repeat::class, 'repeatRemoveNote']); - - return Event::next; - } -} diff --git a/plugins/Repeat/Controller/Repeat.php b/plugins/RepeatNote/Controller/Repeat.php similarity index 58% rename from plugins/Repeat/Controller/Repeat.php rename to plugins/RepeatNote/Controller/Repeat.php index d84d82a5b5..a3e7dd5de6 100644 --- a/plugins/Repeat/Controller/Repeat.php +++ b/plugins/RepeatNote/Controller/Repeat.php @@ -1,6 +1,6 @@ getId(); - $opts = ['actor_id' => $actor_id, 'repeat_of' => $id]; - $note_already_repeated = DB::count('note_repeat', $opts) >= 1; + $actor_id = $user->getId(); + $note = Note::getWithPK(['id' => $id]); - // Before the form is rendered for the first time - if (\is_null($note_already_repeated)) { - throw new ClientException(_m('Note already repeated!')); - } - - $note = Note::getWithPK(['id' => $id]); $form_add_to_repeat = Form::create([ ['add_repeat', SubmitType::class, [ 'label' => _m('Repeat note!'), - 'attr' => [ + 'attr' => [ 'title' => _m('Repeat this note!'), ], ], @@ -80,41 +71,12 @@ class Repeat extends Controller $form_add_to_repeat->handleRequest($request); if ($form_add_to_repeat->isSubmitted()) { - // If the user goes back to the form, again - if (DB::count('note_repeat', ['actor_id' => $actor_id, 'repeat_of' => $id]) >= 1) { - throw new ClientException(_m('Note already repeated!')); - } - - if (!\is_null($note)) { - // Create a new note with the same content as the original - $repeat = Posting::storeLocalNote( - actor: Actor::getById($actor_id), - content: $note->getContent(), - content_type: $note->getContentType(), - language: Language::getById($note->getLanguageId())->getLocale(), - processed_attachments: $note->getAttachmentsWithTitle(), - ); - - // Find the id of the note we just created - $repeat_id = $repeat->getId(); - $og_id = $note->getId(); - - // Add it to note_repeat table - if (!\is_null($repeat_id)) { - DB::persist(NoteRepeat::create([ - 'note_id' => $repeat_id, - 'actor_id' => $actor_id, - 'repeat_of' => $og_id, - ])); - } - - // Update DB one last time - DB::flush(); - } + \Plugin\RepeatNote\RepeatNote::repeatNote(note: $note, actor_id: $actor_id); + DB::flush(); // Redirect user to where they came from // Prevent open redirect - if (!\is_null($from = $this->string('from'))) { + 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) @@ -129,14 +91,14 @@ class Repeat extends Controller } return [ - '_template' => 'repeat/add_to_repeats.html.twig', - 'note' => $note, + '_template' => 'repeat/add_to_repeats.html.twig', + 'note' => $note, 'add_repeat' => $form_add_to_repeat->createView(), ]; } /** - * @throws \App\Util\Exception\ServerException + * @throws ServerException * @throws ClientException * @throws NoLoggedInUser * @throws NoSuchNoteException @@ -144,19 +106,15 @@ class Repeat extends Controller */ public function repeatRemoveNote(Request $request, int $id): array { - $user = Common::ensureLoggedIn(); - $actor_id = $user->getId(); - $opts = ['id' => $id]; - $remove_repeat_note = DB::find('note', $opts); - if (\is_null($remove_repeat_note)) { - throw new NoSuchNoteException(); - } + $user = Common::ensureLoggedIn(); + + $actor_id = $user->getId(); $form_remove_repeat = Form::create([ ['remove_repeat', SubmitType::class, [ 'label' => _m('Remove repeat'), - 'attr' => [ + 'attr' => [ 'title' => _m('Remove note from repeats.'), ], ], @@ -165,21 +123,15 @@ class Repeat extends Controller $form_remove_repeat->handleRequest($request); if ($form_remove_repeat->isSubmitted()) { - if ($remove_repeat_note) { - // Remove the note itself - DB::remove($remove_repeat_note); - DB::flush(); - - // Remove from the note_repeat table - $opts = ['note_id' => $id]; - $remove_note_repeat = DB::find('note_repeat', $opts); - DB::remove($remove_note_repeat); + if (!is_null(\Plugin\RepeatNote\RepeatNote::unrepeatNote(note_id: $id, actor_id: $actor_id))) { DB::flush(); + } else { + throw new ClientException(_m('Note wasn\'t repeated!')); } // Redirect user to where they came from // Prevent open redirect - if (!\is_null($from = $this->string('from'))) { + 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) @@ -194,8 +146,8 @@ class Repeat extends Controller } return [ - '_template' => 'repeat/remove_from_repeats.html.twig', - 'note' => $remove_repeat_note, + '_template' => 'repeat/remove_from_repeats.html.twig', + 'note' => Note::getById($id), 'remove_repeat' => $form_remove_repeat->createView(), ]; } diff --git a/plugins/Repeat/Entity/NoteRepeat.php b/plugins/RepeatNote/Entity/NoteRepeat.php similarity index 96% rename from plugins/Repeat/Entity/NoteRepeat.php rename to plugins/RepeatNote/Entity/NoteRepeat.php index 567b792e67..75c43a0cd1 100644 --- a/plugins/Repeat/Entity/NoteRepeat.php +++ b/plugins/RepeatNote/Entity/NoteRepeat.php @@ -21,7 +21,7 @@ declare(strict_types=1); // }}} -namespace Plugin\Repeat\Entity; +namespace Plugin\RepeatNote\Entity; use App\Core\DB\DB; use App\Core\Entity; @@ -99,13 +99,13 @@ class NoteRepeat extends Entity 'actor_id' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'description' => 'Who made this repeat'], 'repeat_of' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'Note this is a repeat of'], ], - 'primary key' => ['note_id'], + 'primary key' => ['note_id'], 'foreign keys' => [ 'note_id_to_id_fkey' => ['note', ['note_id' => 'id']], 'note_repeat_of_id_fkey' => ['note', ['repeat_of' => 'id']], 'actor_reply_to_id_fkey' => ['actor', ['actor_id' => 'id']], ], - 'indexes' => [ + 'indexes' => [ 'note_repeat_of_idx' => ['repeat_of'], ], ]; diff --git a/plugins/RepeatNote/RepeatNote.php b/plugins/RepeatNote/RepeatNote.php new file mode 100644 index 0000000000..831df5a5aa --- /dev/null +++ b/plugins/RepeatNote/RepeatNote.php @@ -0,0 +1,272 @@ +. +// }}} + +namespace Plugin\RepeatNote; + +use App\Core\DB\DB; +use App\Core\Event; +use App\Core\Modules\NoteHandlerPlugin; +use App\Core\Router\RouteLoader; +use App\Core\Router\Router; +use App\Entity\Activity; +use App\Entity\Actor; +use App\Entity\Language; +use App\Entity\Note; +use App\Util\Common; +use App\Util\Exception\ServerException; +use App\Util\Formatting; +use Component\Posting\Posting; +use Plugin\RepeatNote\Entity\NoteRepeat; +use Symfony\Component\HttpFoundation\Request; +use function App\Core\I18n\_m; +use function count; +use function is_null; +use const SORT_REGULAR; + +class RepeatNote extends NoteHandlerPlugin +{ + public static function repeatNote(Note $note, int $actor_id, string $source = 'web'): Activity + { + $repeat_entity = DB::findBy('note_repeat', [ + 'actor_id' => $actor_id, + 'note_id' => $note->getId(), + ])[0] ?? null; + + if (!is_null($repeat_entity)) { + return DB::findBy('activity', [ + 'actor_id' => $actor_id, + 'verb' => 'repeat', + 'object_type' => 'note', + 'object_id' => $note->getId() + ], order_by: ['created' => 'dsc'])[0]; + } else { + // Create a new note with the same content as the original + $repeat = Posting::storeLocalNote( + actor: Actor::getById($actor_id), + content: $note->getContent(), + content_type: $note->getContentType(), + language: Language::getById($note->getLanguageId())->getLocale(), + processed_attachments: $note->getAttachmentsWithTitle(), + ); + + // Find the id of the note we just created + $repeat_id = $repeat?->getId(); + $og_id = $note->getId(); + + // Add it to note_repeat table + if (!is_null($repeat_id)) { + DB::persist(NoteRepeat::create([ + 'note_id' => $repeat_id, + 'actor_id' => $actor_id, + 'repeat_of' => $og_id, + ])); + } + } + + // Log an activity + $repeat_activity = Activity::create([ + 'actor_id' => $actor_id, + 'verb' => 'repeat', + 'object_type' => 'note', + 'object_id' => $note->getId(), + 'source' => $source, + ]); + DB::persist($repeat_activity); + return $repeat_activity; + } + + public static function unrepeatNote(int $note_id, int $actor_id, string $source = 'web'): ?Activity + { + $already_repeated = DB::findBy('note_repeat', ['actor_id' => $actor_id, 'repeat_of' => $note_id])[0] ?? null; + + if (!is_null($already_repeated)) { // If it was repeated, then we can undo it + // Find previous repeat activity + $already_repeated_activity = DB::findBy('activity', [ + 'actor_id' => $actor_id, + 'verb' => 'repeat', + 'object_type' => 'note', + 'object_id' => $already_repeated->getRepeatOf() + ])[0] ?? null; + + // Remove the clone note + DB::findBy('note', ['id' => $already_repeated->getNoteId()])[0]->delete(); + + // Remove from the note_repeat table + DB::remove(DB::findBy('note_repeat', ['note_id' => $already_repeated->getNoteId()])[0]); + + // Log an activity + $undo_repeat_activity = Activity::create([ + 'actor_id' => $actor_id, + 'verb' => 'undo', + 'object_type' => 'activity', + 'object_id' => $already_repeated_activity->getId(), + 'source' => $source, + ]); + DB::persist($undo_repeat_activity); + return $undo_repeat_activity; + } else { + // Either was undoed already + if (!is_null($already_repeated_activity = DB::findBy('activity', [ + 'actor_id' => $actor_id, + 'verb' => 'repeat', + 'object_type' => 'note', + 'object_id' => $note_id, + ])[0] ?? null)) { + return DB::findBy('activity', [ + 'actor_id' => $actor_id, + 'verb' => 'undo', + 'object_type' => 'activity', + 'object_id' => $already_repeated_activity->getId(), + ])[0] ?? null; // null if not undoed + } else { + // or it's an attempt to undo something that wasn't repeated in the first place, + return null; + } + } + } + + /** + * HTML rendering event that adds the repeat form as a note + * action, if a user is logged in + * + * @param Request $request + * @param Note $note + * @param array $actions + * @return bool Event hook + */ + public function onAddNoteActions(Request $request, Note $note, array &$actions): bool + { + // Only logged users can repeat notes + if (is_null($user = Common::user())) { + return Event::next; + } + + // If note is repeated, "is_repeated" is 1, 0 otherwise. + $is_repeat = ($note_repeat = DB::findBy('note_repeat', [ + 'actor_id' => $user->getId(), + 'note_id' => $note->getId() + ])) !== [] ? 1 : 0; + + // If note was already repeated, do not add the action + try { + if (DB::findOneBy('note_repeat', [ + 'repeat_of' => $note->getId(), + 'actor_id' => $user->getId() + ])) { + return Event::next; + } + } catch (\Exception) { + // It's okay + } + + // Generating URL for repeat action route + $args = ['id' => $is_repeat === 0 ? $note->getId() : $note_repeat[0]->getRepeatOf()]; + $type = Router::ABSOLUTE_PATH; + $repeat_action_url = $is_repeat + ? Router::url('repeat_remove', $args, $type) + : Router::url('repeat_add', $args, $type); + + // TODO clean this up + // SECURITY: open redirect? + $query_string = $request->getQueryString(); + // Concatenating get parameter to redirect the user to where he came from + $repeat_action_url .= !is_null($query_string) ? '?from=' . mb_substr($query_string, 2) : ''; + + $extra_classes = $is_repeat ? 'note-actions-set' : 'note-actions-unset'; + $repeat_action = [ + 'url' => $repeat_action_url, + 'title' => $is_repeat ? 'Remove this repeat' : 'Repeat this note!', + 'classes' => "button-container repeat-button-container {$extra_classes}", + 'id' => 'repeat-button-container-' . $note->getId(), + ]; + + $actions[] = $repeat_action; + return Event::next; + } + + /** + * Append on note information about user actions. + * + * @return array|bool + */ + public function onAppendCardNote(array $vars, array &$result) + { + // if note is the original and user isn't the one who repeated, append on end "user repeated this" + // if user is the one who repeated, append on end "you repeated this, remove repeat?" + $check_user = !is_null(Common::user()); + + $note = $vars['note']; + + $complementary_info = ''; + $repeat_actor = []; + $note_repeats = NoteRepeat::getNoteRepeats($note); + + // Get actors who replied + foreach ($note_repeats as $reply) { + $repeat_actor[] = Actor::getWithPK($reply->getActorId()); + } + if (count($repeat_actor) < 1) { + return Event::next; + } + + // Filter out multiple replies from the same actor + $repeat_actor = array_unique($repeat_actor, SORT_REGULAR); + + // Add to complementary info + foreach ($repeat_actor as $actor) { + $repeat_actor_url = $actor->getUrl(); + $repeat_actor_nickname = $actor->getNickname(); + + if ($check_user && $actor->getId() === (Common::actor())->getId()) { + // If the repeat is yours + try { + $you_translation = _m('You'); + } catch (ServerException $e) { + $you_translation = 'You'; + } + + $prepend = "{$you_translation}, " . ($prepend = &$complementary_info); + $complementary_info = $prepend; + } else { + // If the repeat is from someone else + $complementary_info .= "{$repeat_actor_nickname}, "; + } + } + + $complementary_info = rtrim(trim($complementary_info), ','); + $complementary_info .= ' repeated this note.'; + $result[] = Formatting::twigRenderString($complementary_info, []); + + return $result; + } + + public function onAddRoute(RouteLoader $r): bool + { + // Add/remove note to/from repeats + $r->connect(id: 'repeat_add', uri_path: '/object/note/{id<\d+>}/repeat', target: [Controller\Repeat::class, 'repeatAddNote']); + $r->connect(id: 'repeat_remove', uri_path: '/object/note/{id<\d+>}/unrepeat', target: [Controller\Repeat::class, 'repeatRemoveNote']); + + return Event::next; + } + + // ActivityPub + +} diff --git a/plugins/Repeat/templates/repeat/add_to_repeats.html.twig b/plugins/RepeatNote/templates/repeat/add_to_repeats.html.twig similarity index 100% rename from plugins/Repeat/templates/repeat/add_to_repeats.html.twig rename to plugins/RepeatNote/templates/repeat/add_to_repeats.html.twig diff --git a/plugins/Repeat/templates/repeat/remove_from_repeats.html.twig b/plugins/RepeatNote/templates/repeat/remove_from_repeats.html.twig similarity index 100% rename from plugins/Repeat/templates/repeat/remove_from_repeats.html.twig rename to plugins/RepeatNote/templates/repeat/remove_from_repeats.html.twig