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