From c40866ecf6eb0b05286e01c5681c61d9f4fcd9da Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Sat, 4 Dec 2021 14:09:09 +0000 Subject: [PATCH] [PLUGIN][TagBasedFiltering] Add TagBasedFiltering plugin, which allows filtering feeds by note tags and (soon) actor tags --- .../Controller/TagBasedFiltering.php | 153 ++++++++++++++++++ .../TagBasedFiltering/TagBasedFiltering.php | 91 +++++++++++ .../tag-based-filtering/edit-tags.html.twig | 16 ++ 3 files changed, 260 insertions(+) create mode 100644 plugins/TagBasedFiltering/Controller/TagBasedFiltering.php create mode 100644 plugins/TagBasedFiltering/TagBasedFiltering.php create mode 100644 plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig diff --git a/plugins/TagBasedFiltering/Controller/TagBasedFiltering.php b/plugins/TagBasedFiltering/Controller/TagBasedFiltering.php new file mode 100644 index 0000000000..377291f91f --- /dev/null +++ b/plugins/TagBasedFiltering/Controller/TagBasedFiltering.php @@ -0,0 +1,153 @@ +. + +// }}} + +namespace Plugin\TagBasedFiltering\Controller; + +use App\Core\Cache; +use App\Core\Controller; +use App\Core\DB\DB; +use App\Core\Form; +use function App\Core\I18n\_m; +use App\Entity\Language; +use App\Entity\Note; +use App\Entity\NoteTag; +use App\Entity\NoteTagBlock; +use App\Util\Common; +use App\Util\Exception\NotImplementedException; +use App\Util\Exception\RedirectException; +use Component\Tag\Tag; +use Functional as F; +use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\SubmitButton; +use Symfony\Component\HttpFoundation\Request; + +class TagBasedFiltering extends Controller +{ + /** + * Edit the user's list of blocked note tags, with the option of adding the notes from the note $note_id, if given + */ + public function editBlockedNoteTags(Request $request, ?int $note_id) + { + $user = Common::ensureLoggedIn(); + $note = !\is_null($note_id) ? Note::getFromId($note_id) : null; + $note_tag_blocks = NoteTagBlock::getFromActorId($user->getId()); + $note_tags = !\is_null($note_id) ? NoteTag::getFromNoteId($note_id) : []; + $blockable_note_tags = F\reject( + $note_tags, + fn (NoteTag $nt) => NoteTagBlock::checkBlocksNoteTag($nt, $note_tag_blocks), + ); + + $new_tags_form_definition = []; + + foreach ($blockable_note_tags as $nt) { + $canon = $nt->getCanonical(); + $new_tags_form_definition[] = ["{$canon}:new-tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']]; + $new_tags_form_definition[] = ["{$canon}:use-canon", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]]; + $new_tags_form_definition[] = ["{$canon}:add", SubmitType::class, ['label' => _m('Block')]]; + } + + $existing_tags_form_definition = []; + foreach ($note_tag_blocks as $ntb) { + $canon = $ntb->getCanonical(); + $existing_tags_form_definition[] = ["{$canon}:old-tag", TextType::class, ['data' => '#' . $ntb->getTag(), 'label' => ' ', 'disabled' => true]]; + $existing_tags_form_definition[] = ["{$canon}:toggle-canon", SubmitType::class, ['label' => $ntb->getUseCanonical() ? _m('Set non-canonical') : _m('Set canonical')]]; + $existing_tags_form_definition[] = ["{$canon}:remove", SubmitType::class, ['label' => _m('Unblock')]]; + } + + $new_tags_form = null; + if (!empty($new_tags_form_definition)) { + $new_tags_form = Form::create($new_tags_form_definition); + $new_tags_form->handleRequest($request); + if ($new_tags_form->isSubmitted() && $new_tags_form->isValid()) { + $data = $new_tags_form->getData(); + foreach ($new_tags_form_definition as [$id, $_, $opts]) { + [$canon, $type] = explode(':', $id); + if ($type === 'add') { + /** @var SubmitButton $button */ + $button = $new_tags_form->get($id); + if ($button->isClicked()) { + Cache::delete(NoteTagBlock::cacheKey($user->getId())); + Cache::delete(TagFilerPlugin::cacheKeys($user->getId())['note']); + $new_tag = Tag::ensureValid($data[$canon . ':new-tag']); + $canonical_tag = Tag::canonicalTag($new_tag, Language::getFromNote($note)->getLocale()); + DB::persist(NoteTagBlock::create([ + 'blocker' => $user->getId(), + 'tag' => $new_tag, + 'canonical' => $canonical_tag, + 'use_canonical' => $data[$canon . ':use-canon'], + ])); + DB::flush(); + throw new RedirectException; + } + } + } + } + } + + $existing_tags_form = null; + if (!empty($existing_tags_form_definition)) { + $existing_tags_form = Form::create($existing_tags_form_definition); + $existing_tags_form->handleRequest($request); + if ($existing_tags_form->isSubmitted() && $existing_tags_form->isValid()) { + $data = $existing_tags_form->getData(); + foreach ($existing_tags_form_definition as [$id, $_, $opts]) { + [$canon, $type] = explode(':', $id); + if (\in_array($type, ['remove', 'toggle-canon'])) { + /** @var SubmitButton $button */ + $button = $existing_tags_form->get($id); + if ($button->isClicked()) { + Cache::delete(NoteTagBlock::cacheKey($user->getId())); + Cache::delete(TagFilerPlugin::cacheKeys($user->getId())['note']); + switch ($type) { + case 'toggle-canon': + $ntb = DB::getReference('note_tag_block', ['blocker' => $user->getId(), 'canonical' => $canon]); + $ntb->setUseCanonical(!$ntb->getUseCanonical()); + DB::flush(); + throw new RedirectException; + case 'remove': + $old_tag = $data[$canon . ':old-tag']; + DB::removeBy('note_tag_block', ['blocker' => $user->getId(), 'canonical' => $canon]); + throw new RedirectException; + } + } + } + } + } + } + + return [ + '_template' => 'tag-based-filtering/edit-tags.html.twig', + 'note' => !\is_null($note_id) ? Note::getFromId($note_id) : null, + 'new_tags_form' => $new_tags_form?->createView(), + 'existing_tags_form' => $existing_tags_form?->createView(), + ]; + } + + public function editBlockedActorTags(Request $request, ?int $note_id) + { + throw new NotImplementedException; + } +} diff --git a/plugins/TagBasedFiltering/TagBasedFiltering.php b/plugins/TagBasedFiltering/TagBasedFiltering.php new file mode 100644 index 0000000000..0ec9919b2e --- /dev/null +++ b/plugins/TagBasedFiltering/TagBasedFiltering.php @@ -0,0 +1,91 @@ +. + +// }}} + +namespace Plugin\TagBasedFiltering; + +use App\Core\Cache; +use App\Core\DB\DB; +use App\Core\Event; +use function App\Core\I18n\_m; +use App\Core\Modules\Plugin; +use App\Core\Router\RouteLoader; +use App\Core\Router\Router; +use App\Entity\Actor; +use App\Entity\LocalUser; +use App\Entity\Note; +use App\Entity\NoteTag; +use App\Entity\NoteTagBlock; +use Functional as F; +use Symfony\Component\HttpFoundation\Request; + +class TagBasedFiltering extends Plugin +{ + public const NOTE_TAG_FILTER_ROUTE = 'filter_feeds_edit_blocked_note_tags'; + public const ACTOR_TAG_FILTER_ROUTE = 'filter_feeds_edit_blocked_actor_tags'; + + public static function cacheKeys(int|LocalUser|Actor $actor_id): array + { + if (!\is_int($actor_id)) { + $actor_id = $actor_id->getId(); + } + return ['note' => "filtered-tags-{$actor_id}"]; + } + + public function onAddRoute(RouteLoader $r) + { + $r->connect(id: self::NOTE_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-note-tags/{note_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedNoteTags']); + $r->connect(id: self::ACTOR_TAG_FILTER_ROUTE, uri_path: '/filter/edit-blocked-actor-tags/{note_id<\d+>?}', target: [Controller\TagBasedFiltering::class, 'editBlockedActorTags']); + return Event::next; + } + + public function onAddExtraNoteActions(Request $request, Note $note, array &$actions) + { + $actions[] = [ + 'title' => _m('Block note tags'), + 'classes' => '', + 'url' => Router::url(self::NOTE_TAG_FILTER_ROUTE, ['note_id' => $note->getId()]), + ]; + + $actions[] = [ + 'title' => _m('Block people tags'), + 'classes' => '', + 'url' => Router::url(self::ACTOR_TAG_FILTER_ROUTE, ['note_id' => $note->getId()]), + ]; + } + + public function onFilterNoteList(Actor $actor, array $notes, ?array &$notes_out) + { + $blocked_note_tags = Cache::get( + self::cacheKeys($actor)['note'], + fn () => DB::dql('select ntb from note_tag_block ntb where ntb.blocker = :blocker', ['blocker' => $actor->getId()]), + ); + $notes_out = F\reject( + $notes, + fn (Note $n) => F\some( + dump(NoteTag::getFromNoteId($n->getId())), + fn ($nt) => NoteTagBlock::checkBlocksNoteTag($nt, $blocked_note_tags), + ), + ); + return Event::next; + } +} diff --git a/plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig b/plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig new file mode 100644 index 0000000000..8bf2fd9b13 --- /dev/null +++ b/plugins/TagBasedFiltering/templates/tag-based-filtering/edit-tags.html.twig @@ -0,0 +1,16 @@ +{% extends 'base.html.twig' %} +{% import '/cards/note/view.html.twig' as noteView %} + +{% block body %} + {% if new_tags_form is not null %} +
+ {{ noteView.macro_note(note, {}) }} +
+

{% trans %}Tags in the note above:{% endtrans %}

+ {{ form(new_tags_form) }} + {% endif %} + {% if existing_tags_form is not null %} +

{% trans %}Tags you already blocked:{% endtrans %}

+ {{ form(existing_tags_form) }} + {% endif %} +{% endblock %}