[COMPONENT][Tag] Improve Note Tag Handling and start extracting Circles logic out of the plugin, various bug fixes

This commit is contained in:
Diogo Peralta Cordeiro 2022-01-04 22:20:12 +00:00
parent ee007befa4
commit 627d92b290
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
13 changed files with 180 additions and 473 deletions

View File

@ -48,7 +48,7 @@ class Search extends FeedController
$language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null; $language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null;
$q = $this->string('q'); $q = $this->string('q');
$data = $this->query(query: $q, language: $language); $data = $this->query(query: $q, locale: $language);
$notes = $data['notes']; $notes = $data['notes'];
$actors = $data['actors']; $actors = $data['actors'];

View File

@ -6,31 +6,34 @@ namespace Component\Tag\Controller;
use App\Core\Cache; use App\Core\Cache;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB\DB;
use function App\Core\I18n\_m;
use App\Entity as E;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\BugFoundException; use Component\Language\Entity\Language;
use App\Util\Exception\ClientException;
use App\Util\Exception\RedirectException;
use App\Util\Formatting;
use Component\Tag\Form\SelfTagsForm;
use Component\Tag\Tag as CompTag; use Component\Tag\Tag as CompTag;
use Symfony\Component\Form\SubmitButton;
use Symfony\Component\HttpFoundation\Request;
class Tag extends Controller class Tag extends Controller
{ {
private function process(string|array $canon_single_or_multi, null|string|array $tag_single_or_multi, string $key, string $query, string $template) // TODO: Use Feed::query
// TODO: If ?canonical=something, respect
// TODO: Allow to set locale of tag being selected
private function process(null|string|array $tag_single_or_multi, string $key, string $query, string $template, bool $include_locale = false)
{ {
$actor = Common::actor(); $actor = Common::actor();
$page = $this->int('page') ?: 1; $page = $this->int('page') ?: 1;
$lang = $this->string('lang');
$query_args = ['tag' => $tag_single_or_multi];
if ($include_locale) {
if (!\is_null($locale = $this->string('locale'))) {
$query_args['language_id'] = Language::getByLocale($locale)->getId();
} else {
$query_args['language_id'] = Common::actor()->getTopLanguage()->getId();
}
}
$results = Cache::pagedStream( $results = Cache::pagedStream(
key: $key, key: $key,
query: $query, query: $query,
query_args: ['canon' => $canon_single_or_multi], query_args: $query_args,
actor: $actor, actor: $actor,
page: $page, page: $page,
); );
@ -43,179 +46,25 @@ class Tag extends Controller
]; ];
} }
public function single_note_tag(string $canon) public function single_note_tag(string $tag)
{ {
return $this->process( return $this->process(
canon_single_or_multi: $canon, tag_single_or_multi: $tag,
tag_single_or_multi: $this->string('tag'), key: CompTag::cacheKeys($tag)['note_single'],
key: CompTag::cacheKeys($canon)['note_single'], query: 'SELECT n FROM note AS n JOIN note_tag AS nt WITH n.id = nt.note_id WHERE nt.tag = :tag AND nt.language_id = :language_id ORDER BY nt.created DESC, nt.note_id DESC',
query: 'select n from note n join note_tag nt with n.id = nt.note_id where nt.canonical = :canon order by nt.created DESC, nt.note_id DESC',
template: 'note_tag_feed.html.twig', template: 'note_tag_feed.html.twig',
include_locale: true,
); );
} }
public function multi_note_tags(string $canons) public function multi_note_tags(string $tags)
{ {
return $this->process( return $this->process(
canon_single_or_multi: explode(',', $canons), tag_single_or_multi: explode(',', $tags),
tag_single_or_multi: !\is_null($this->string('tags')) ? explode(',', $this->string('tags')) : null, key: CompTag::cacheKeys(str_replace(',', '-', $tags))['note_multi'],
key: CompTag::cacheKeys(str_replace(',', '-', $canons))['note_multi'], query: 'select n from note n join note_tag nt with n.id = nt.note_id where nt.tag in (:tag) AND nt.language_id = :language_id order by nt.created DESC, nt.note_id DESC',
query: 'select n from note n join note_tag nt with n.id = nt.note_id where nt.canonical in (:canon) order by nt.created DESC, nt.note_id DESC',
template: 'note_tag_feed.html.twig', template: 'note_tag_feed.html.twig',
include_locale: true,
); );
} }
public function single_actor_tag(string $canon)
{
return $this->process(
canon_single_or_multi: $canon,
tag_single_or_multi: $this->string('tag'),
key: CompTag::cacheKeys($canon)['actor_single'],
query: 'select a from actor a join actor_tag at with a.id = at.tagged where at.canonical = :canon order by at.modified DESC',
template: 'actor_tag_feed.html.twig',
);
}
public function multi_actor_tag(string $canons)
{
return $this->process(
canon_single_or_multi: explode(',', $canons),
tag_single_or_multi: !\is_null($this->string('tags')) ? explode(',', $this->string('tags')) : null,
key: CompTag::cacheKeys(str_replace(',', '-', $canons))['actor_multi'],
query: 'select a from actor a join actor_tag at with a.id = at.tagged where at.canonical = :canon order by at.modified DESC',
template: 'actor_tag_feed.html.twig',
);
}
/**
* Generic settings page for an Actor's self tags
*/
public static function settingsSelfTags(Request $request, E\Actor $target, string $details_id)
{
$actor = Common::actor();
if (!$actor->canAdmin($target)) {
throw new ClientException(_m('You don\'t have enough permissions to edit {nickname}\'s settings', ['{nickname}' => $target->getNickname()]));
}
$actor_tags = $target->getSelfTags();
[$add_form, $existing_form] = SelfTagsForm::handleTags(
$request,
$actor_tags,
handle_new: /**
* Handle adding tags
*/
function ($form) use ($request, $target, $details_id) {
$data = $form->getData();
$tags = $data['new-tags'];
$language = $target->getTopLanguage()->getLocale();
foreach ($tags as $tag) {
$tag = CompTag::ensureValid($tag);
$canon_tag = CompTag::canonicalTag($tag, language: $language);
$use_canon = $data['new-tags-use-canon'];
[$actor_tag, $actor_tag_existed] = E\ActorTag::createOrUpdate([
'tagger' => $target->getId(),
'tagged' => $target->getId(),
'tag' => $tag,
'canonical' => $canon_tag,
'use_canonical' => $use_canon,
]);
DB::persist($actor_tag);
$actor_circle = DB::findBy(
'actor_circle',
[
'tagger' => null,
'tagged' => $target->getId(),
'in' => ['tag' => [$tag, $canon_tag]],
'use_canonical' => $use_canon,
],
);
if (empty($actor_circle)) {
if ($actor_tag_existed) {
throw new BugFoundException('Actor tag existed but generic actor circle did not');
}
DB::persist(E\ActorCircle::create([
'tagger' => null,
'tagged' => $target->getId(),
'tag' => $use_canon ? $canon_tag : $tag,
'use_canonical' => $use_canon,
'private' => false,
'description' => null,
]));
}
}
DB::flush();
Cache::delete(E\Actor::cacheKeys($target->getId(), $target->getId())['tags']);
throw new RedirectException($request->get('_route'), ['nickname' => $target->getNickname(), 'open' => $details_id]);
},
handle_existing: /**
* Handle changes to the existing tags
*/
function ($form, array $form_definition) use ($request, $target, $details_id) {
$data = $form->getData();
$changed = false;
$language = $target->getTopLanguage()->getLocale();
foreach (array_chunk($form_definition, 3) as $entry) {
$tag = Formatting::removePrefix($entry[0][2]['data'], '#');
$canon_tag = CompTag::canonicalTag($tag, language: $language);
$use_canon = $entry[1][2]['attr']['data'];
/** @var SubmitButton $remove */
$remove = $form->get($entry[2][0]);
if ($remove->isClicked()) {
$changed = true;
DB::removeBy(
'actor_tag',
[
'tagger' => $target->getId(),
'tagged' => $target->getId(),
'tag' => $tag,
'use_canonical' => $use_canon,
],
);
DB::removeBy(
'actor_circle',
[
'tagger' => null,
'tagged' => $target->getId(),
'tag' => $use_canon ? $canon_tag : $tag,
'use_canonical' => $use_canon,
],
);
}
/** @var SubmitButton $toggle_canon */
$toggle_canon = $form->get($entry[1][0]);
if ($toggle_canon->isSubmitted()) {
$changed = true;
$actor_tag = DB::find(
'actor_tag',
[
'tagger' => $target->getId(),
'tagged' => $target->getId(),
'tag' => $tag,
'use_canonical' => $use_canon,
],
);
DB::persist($actor_tag->setUseCanonical(!$use_canon));
}
}
if ($changed) {
DB::flush();
Cache::delete(E\Actor::cacheKeys($target->getId(), $target->getId())['tags']);
throw new RedirectException($request->get('_route'), ['nickname' => $target->getNickname(), 'open' => $details_id]);
}
},
remove_label: _m('Remove self tag'),
add_label: _m('Add self tag'),
);
return [
'_template' => 'self_tags_settings.fragment.html.twig',
'add_self_tags_form' => $add_form->createView(),
'existing_self_tags_form' => $existing_form?->createView(),
];
}
} }

View File

@ -19,12 +19,15 @@ declare(strict_types = 1);
// along with GNU social. If not, see <http://www.gnu.org/licenses/>. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}} // }}}
namespace App\Entity; namespace Component\Tag\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Entity\Actor;
use App\Entity\Note;
use Component\Language\Entity\Language;
use Component\Tag\Tag; use Component\Tag\Tag;
use DateTimeInterface; use DateTimeInterface;
@ -39,6 +42,7 @@ use DateTimeInterface;
* @author Mikael Nordfeldth <mmn@hethane.se> * @author Mikael Nordfeldth <mmn@hethane.se>
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
* @author Hugo Sales <hugo@hsal.es> * @author Hugo Sales <hugo@hsal.es>
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/ */
@ -51,11 +55,11 @@ class NoteTag extends Entity
private int $note_id; private int $note_id;
private bool $use_canonical; private bool $use_canonical;
private ?int $language_id = null; private ?int $language_id = null;
private \DateTimeInterface $created; private DateTimeInterface $created;
public function setTag(string $tag): self public function setTag(string $tag): self
{ {
$this->tag = \mb_substr($tag, 0, 64); $this->tag = mb_substr($tag, 0, 64);
return $this; return $this;
} }
@ -66,7 +70,7 @@ class NoteTag extends Entity
public function setCanonical(string $canonical): self public function setCanonical(string $canonical): self
{ {
$this->canonical = \mb_substr($canonical, 0, 64); $this->canonical = mb_substr($canonical, 0, 64);
return $this; return $this;
} }
@ -108,13 +112,13 @@ class NoteTag extends Entity
return $this->language_id; return $this->language_id;
} }
public function setCreated(\DateTimeInterface $created): self public function setCreated(DateTimeInterface $created): self
{ {
$this->created = $created; $this->created = $created;
return $this; return $this;
} }
public function getCreated(): \DateTimeInterface public function getCreated(): DateTimeInterface
{ {
return $this->created; return $this->created;
} }
@ -132,15 +136,24 @@ class NoteTag extends Entity
public static function getByNoteId(int $note_id): array public static function getByNoteId(int $note_id): array
{ {
return Cache::getList(self::cacheKey($note_id), fn () => DB::dql('select nt from note_tag nt join note n with n.id = nt.note_id where n.id = :id', ['id' => $note_id])); return Cache::getList(self::cacheKey($note_id), fn () => DB::dql('SELECT nt FROM note_tag AS nt JOIN note AS n WITH n.id = nt.note_id WHERE n.id = :id', ['id' => $note_id]));
} }
public function getUrl(?Actor $actor = null, int $type = Router::ABSOLUTE_PATH): string public function getUrl(?Actor $actor = null, int $type = Router::ABSOLUTE_PATH): string
{ {
$params = ['canon' => $this->getCanonical(), 'tag' => $this->getTag()]; $params['tag'] = $this->getTag();
if (!\is_null($actor)) {
$params['lang'] = $actor->getTopLanguage()->getLocale(); if (\is_null($this->getLanguageId())) {
if (!\is_null($actor)) {
$params['locale'] = $actor->getTopLanguage()->getLocale();
}
} else {
$params['locale'] = Language::getById($this->getLanguageId())->getLocale();
} }
if ($this->getUseCanonical()) {
$params['canonical'] = $this->getCanonical();
}
return Router::url(id: 'single_note_tag', args: $params, type: $type); return Router::url(id: 'single_note_tag', args: $params, type: $type);
} }
@ -150,18 +163,18 @@ class NoteTag extends Entity
'name' => 'note_tag', 'name' => 'note_tag',
'description' => 'Hash tags on notes', 'description' => 'Hash tags on notes',
'fields' => [ 'fields' => [
'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to tagged note'],
'tag' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'hash tag associated with this note'], 'tag' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'hash tag associated with this note'],
'canonical' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'ascii slug of tag'], 'canonical' => ['type' => 'varchar', 'length' => Tag::MAX_TAG_LENGTH, 'not null' => true, 'description' => 'ascii slug of tag'],
'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to tagged note'],
'use_canonical' => ['type' => 'bool', 'not null' => true, 'description' => 'whether the user wanted to use canonical tags in this note. Separate for blocks'], 'use_canonical' => ['type' => 'bool', 'not null' => true, 'description' => 'whether the user wanted to use canonical tags in this note. Separate for blocks'],
'language_id' => ['type' => 'int', 'not null' => false, 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'many to many', 'description' => 'the language this entry refers to'], 'language_id' => ['type' => 'int', 'not null' => false, 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'many to many', 'description' => 'the language this entry refers to'],
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
], ],
'primary key' => ['tag', 'note_id'], 'primary key' => ['note_id', 'tag'], // No need to require language in this association because all the related tags will be in the note's language already
'indexes' => [ 'indexes' => [
'note_tag_created_idx' => ['created'], 'note_tag_created_idx' => ['created'],
'note_tag_note_id_idx' => ['note_id'], 'note_tag_note_id_idx' => ['note_id'],
'note_tag_canonical_idx' => ['canonical'], 'note_tag_tag_language_id_idx' => ['tag', 'language_id'],
'note_tag_tag_created_note_id_idx' => ['tag', 'created', 'note_id'], 'note_tag_tag_created_note_id_idx' => ['tag', 'created', 'note_id'],
], ],
]; ];

View File

@ -19,7 +19,7 @@ declare(strict_types = 1);
// along with GNU social. If not, see <http://www.gnu.org/licenses/>. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}} // }}}
namespace App\Entity; namespace Component\Tag\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB\DB; use App\Core\DB\DB;
@ -46,7 +46,7 @@ class NoteTagBlock extends Entity
private string $tag; private string $tag;
private string $canonical; private string $canonical;
private bool $use_canonical; private bool $use_canonical;
private \DateTimeInterface $modified; private DateTimeInterface $modified;
public function setBlocker(int $blocker): self public function setBlocker(int $blocker): self
{ {
@ -61,7 +61,7 @@ class NoteTagBlock extends Entity
public function setTag(string $tag): self public function setTag(string $tag): self
{ {
$this->tag = \mb_substr($tag, 0, 64); $this->tag = mb_substr($tag, 0, 64);
return $this; return $this;
} }
@ -72,7 +72,7 @@ class NoteTagBlock extends Entity
public function setCanonical(string $canonical): self public function setCanonical(string $canonical): self
{ {
$this->canonical = \mb_substr($canonical, 0, 64); $this->canonical = mb_substr($canonical, 0, 64);
return $this; return $this;
} }
@ -92,13 +92,13 @@ class NoteTagBlock extends Entity
return $this->use_canonical; return $this->use_canonical;
} }
public function setModified(\DateTimeInterface $modified): self public function setModified(DateTimeInterface $modified): self
{ {
$this->modified = $modified; $this->modified = $modified;
return $this; return $this;
} }
public function getModified(): \DateTimeInterface public function getModified(): DateTimeInterface
{ {
return $this->modified; return $this->modified;
} }

View File

@ -1,63 +0,0 @@
<?php
declare(strict_types = 1);
namespace Component\Tag\Form;
use App\Core\Form;
use function App\Core\I18n\_m;
use App\Entity as E;
use App\Util\Form\ArrayTransformer;
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\HttpFoundation\Request;
abstract class SelfTagsForm
{
/**
* @param E\ActorTag[]|E\ActorTagBlock[]|E\NoteTagBlock[] $tags
*
* @return array [Form (add), ?Form (existing)]
*/
public static function handleTags(
Request $request,
array $tags,
callable $handle_new,
callable $handle_existing,
string $remove_label,
string $add_label,
): array {
$form_definition = [];
foreach ($tags as $tag) {
$canon = $tag->getCanonical();
$form_definition[] = ["{$canon}:old-tag", TextType::class, ['data' => '#' . $tag->getTag(), 'label' => ' ', 'disabled' => true]];
$form_definition[] = ["{$canon}:toggle-canon", SubmitType::class, ['attr' => ['data' => $tag->getUseCanonical()], 'label' => $tag->getUseCanonical() ? _m('Set non-canonical') : _m('Set canonical')]];
$form_definition[] = [$existing_form_name = "{$canon}:remove", SubmitType::class, ['label' => $remove_label]];
}
$existing_form = !empty($form_definition) ? Form::create($form_definition) : null;
$add_form = Form::create([
['new-tags', TextType::class, ['label' => ' ', 'data' => [], 'required' => false, 'help' => _m('Tags for yourself (letters, numbers, -, ., and _), comma- or space-separated.'), 'transformer' => ArrayTransformer::class]],
['new-tags-use-canon', CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Assume this tag is the same as similar tags'), 'required' => false, 'data' => true]],
[$add_form_name = 'new-tags-add', SubmitType::class, ['label' => $add_label]],
]);
if ($request->getMethod() === 'POST' && $request->request->has($add_form_name)) {
$add_form->handleRequest($request);
if ($add_form->isSubmitted() && $add_form->isValid()) {
$handle_new($add_form);
}
}
if (!\is_null($existing_form) && $request->getMethod() === 'POST' && $request->request->has($existing_form_name ?? '')) {
$existing_form->handleRequest($request);
if ($existing_form->isSubmitted() && $existing_form->isValid()) {
$handle_existing($existing_form, $form_definition);
}
}
return [$add_form, $existing_form];
}
}

View File

@ -30,18 +30,15 @@ use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\ActorCircle;
use App\Entity\ActorTag;
use App\Entity\Note; use App\Entity\Note;
use App\Entity\NoteTag;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Formatting; use App\Util\Formatting;
use App\Util\Functional as GSF; use App\Util\Functional as GSF;
use App\Util\HTML; use App\Util\HTML;
use App\Util\Nickname; use Component\Circle\Entity\ActorTag;
use Component\Language\Entity\Language; use Component\Language\Entity\Language;
use Component\Tag\Controller as C; use Component\Tag\Entity\NoteTag;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@ -53,22 +50,20 @@ use Symfony\Component\HttpFoundation\Request;
* Component responsible for extracting tags from posted notes, as well as normalizing them * Component responsible for extracting tags from posted notes, as well as normalizing them
* *
* @author Hugo Sales <hugo@hsal.es> * @author Hugo Sales <hugo@hsal.es>
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/ */
class Tag extends Component class Tag extends Component
{ {
public const MAX_TAG_LENGTH = 64; public const MAX_TAG_LENGTH = 64;
public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags
public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/'; public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,64}';
public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,64}';
public function onAddRoute($r): bool public function onAddRoute($r): bool
{ {
$r->connect('single_note_tag', '/note-tag/{canon<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']); $r->connect('single_note_tag', '/note-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']);
$r->connect('multi_note_tags', '/note-tags/{canons<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']); $r->connect('multi_note_tags', '/note-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']);
$r->connect('single_actor_tag', '/actor-tag/{canon<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_actor_tag']);
$r->connect('multi_actor_tags', '/actor-tags/{canons<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_actor_tags']);
return Event::next; return Event::next;
} }
@ -86,7 +81,10 @@ class Tag extends Component
preg_match_all(self::TAG_REGEX, $content, $matched_tags, \PREG_SET_ORDER); preg_match_all(self::TAG_REGEX, $content, $matched_tags, \PREG_SET_ORDER);
$matched_tags = array_unique(F\map($matched_tags, fn ($m) => $m[2])); $matched_tags = array_unique(F\map($matched_tags, fn ($m) => $m[2]));
foreach ($matched_tags as $match) { foreach ($matched_tags as $match) {
$tag = self::ensureValid($match); $tag = self::extract($match);
if (!self::validate($tag)) {
continue; // Ignore invalid tag candidates
}
$canonical_tag = self::canonicalTag($tag, \is_null($lang_id = $note->getLanguageId()) ? null : Language::getById($lang_id)->getLocale()); $canonical_tag = self::canonicalTag($tag, \is_null($lang_id = $note->getLanguageId()) ? null : Language::getById($lang_id)->getLocale());
DB::persist(NoteTag::create([ DB::persist(NoteTag::create([
'tag' => $tag, 'tag' => $tag,
@ -103,38 +101,54 @@ class Tag extends Component
return Event::next; return Event::next;
} }
public function onRenderPlainTextNoteContent(string &$text, ?string $language = null): bool public function onRenderPlainTextNoteContent(string &$text, ?string $locale = null): bool
{ {
$text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $language), $text); $text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $locale), $text);
return Event::next; return Event::next;
} }
public static function cacheKeys(string $canon_single_or_multi): array public static function cacheKeys(string $tag_single_or_multi): array
{ {
return [ return [
'note_single' => "note-tag-feed-{$canon_single_or_multi}", 'note_single' => "note-tag-feed-{$tag_single_or_multi}",
'note_multi' => "note-tags-feed-{$canon_single_or_multi}", 'note_multi' => "note-tags-feed-{$tag_single_or_multi}",
'actor_single' => "actor-tag-feed-{$canon_single_or_multi}", 'actor_single' => "actor-tag-feed-{$tag_single_or_multi}",
'actor_multi' => "actor-tags-feed-{$canon_single_or_multi}", 'actor_multi' => "actor-tags-feed-{$tag_single_or_multi}",
]; ];
} }
private static function tagLink(string $tag, ?string $language): string private static function tagLink(string $tag, ?string $locale): string
{ {
$tag = self::ensureLength($tag); $tag = self::extract($tag);
$canonical = self::canonicalTag($tag, $language); $url = Router::url('single_note_tag', !\is_null($locale) ? ['tag' => $tag, 'locale' => $locale] : ['tag' => $tag]);
$url = Router::url('single_note_tag', !\is_null($language) ? ['canon' => $canonical, 'lang' => $language, 'tag' => $tag] : ['canon' => $canonical, 'tag' => $tag]); return HTML::html(['span' => ['attrs' => ['class' => 'tag'],
return HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => $tag, 'rel' => 'tag'], $tag]], options: ['indent' => false]); '#' . HTML::html(['a' => [
'attrs' => [
'href' => $url,
'rel' => 'tag', // https://microformats.org/wiki/rel-tag
],
$tag,
]], options: ['indent' => false]),
]], options: ['indent' => false, 'raw' => true]);
} }
public static function ensureValid(string $tag) public static function extract(string $tag): string
{ {
$tag = self::ensureLength(Formatting::removePrefix($tag, '#')); return self::ensureLength(Formatting::removePrefix($tag, '#'));
if (preg_match(self::TAG_REGEX, '#' . $tag)) { }
return $tag;
} else { public static function validate(string $tag): bool
{
return preg_match(self::TAG_REGEX, '#' . $tag) === 1;
}
public static function sanitize(string $tag): string
{
$tag = self::extract($tag);
if (!self::validate($tag)) {
throw new ClientException(_m('Invalid tag given: {tag}', ['{tag}' => $tag])); throw new ClientException(_m('Invalid tag given: {tag}', ['{tag}' => $tag]));
} }
return $tag;
} }
public static function ensureLength(string $tag): string public static function ensureLength(string $tag): string
@ -143,11 +157,11 @@ class Tag extends Component
} }
/** /**
* Convert a tag to it's canonical representation, by splitting it * Convert a tag to its canonical representation, by splitting it
* into words, stemming it in the given language (if enabled) and * into words, stemming it in the given language (if enabled) and
* sluggifying it (turning it into an ASCII representation) * sluggifying it (turning it into an ASCII representation)
*/ */
public static function canonicalTag(string $tag, ?string $language): string public static function canonicalTag(string $tag, ?string $language = null): string
{ {
$result = ''; $result = '';
foreach (Formatting::splitWords(str_replace('#', '', $tag)) as $word) { foreach (Formatting::splitWords(str_replace('#', '', $tag)) as $word) {
@ -165,17 +179,20 @@ class Tag extends Component
* *
* $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor * $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor
*/ */
public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, ?string $language, ?Actor $actor, &$note_expr, &$actor_expr): bool public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool
{ {
if (!str_contains($term, ':')) { if (!str_contains($term, ':')) {
return Event::next; return Event::next;
} }
if (\is_null($locale)) {
$locale = Common::currentLanguage();
}
[$search_type, $search_term] = explode(':', $term); [$search_type, $search_term] = explode(':', $term);
if (str_starts_with($search_term, '#')) { if (str_starts_with($search_term, '#')) {
$search_term = self::ensureValid($search_term); $search_term = self::sanitize($search_term);
$canon_search_term = self::canonicalTag($search_term, $language); $canonical_search_term = self::canonicalTag($search_term, $locale);
$temp_note_expr = $eb->eq('note_tag.canonical', $canon_search_term); $temp_note_expr = $eb->eq('note_tag.canonical', $canonical_search_term);
$temp_actor_expr = $eb->eq('actor_tag.canonical', $canon_search_term); $temp_actor_expr = $eb->eq('actor_tag.canonical', $canonical_search_term);
if (Formatting::startsWith($term, ['note:', 'tag:', 'people:'])) { if (Formatting::startsWith($term, ['note:', 'tag:', 'people:'])) {
$note_expr = $temp_note_expr; $note_expr = $temp_note_expr;
} elseif (Formatting::startsWith($term, ['people:', 'actor:'])) { } elseif (Formatting::startsWith($term, ['people:', 'actor:'])) {
@ -183,7 +200,7 @@ class Tag extends Component
} elseif (Formatting::startsWith($term, GSF::cartesianProduct([['people', 'actor'], ['circle', 'list'], [':']], separator: ['-', '_']))) { } elseif (Formatting::startsWith($term, GSF::cartesianProduct([['people', 'actor'], ['circle', 'list'], [':']], separator: ['-', '_']))) {
$null_tagger_expr = $eb->isNull('actor_circle.tagger'); $null_tagger_expr = $eb->isNull('actor_circle.tagger');
$tagger_expr = \is_null($actor_expr) ? $null_tagger_expr : $eb->orX($null_tagger_expr, $eb->eq('actor_circle.tagger', $actor->getId())); $tagger_expr = \is_null($actor_expr) ? $null_tagger_expr : $eb->orX($null_tagger_expr, $eb->eq('actor_circle.tagger', $actor->getId()));
$tags = array_unique([$search_term, $canon_search_term]); $tags = array_unique([$search_term, $canonical_search_term]);
$tag_expr = \count($tags) === 1 ? $eb->eq('actor_circle.tag', $tags[0]) : $eb->in('actor_circle.tag', $tags); $tag_expr = \count($tags) === 1 ? $eb->eq('actor_circle.tag', $tags[0]) : $eb->in('actor_circle.tag', $tags);
$search_expr = $eb->andX( $search_expr = $eb->andX(
$tagger_expr, $tagger_expr,
@ -202,52 +219,23 @@ class Tag extends Component
public function onSearchQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool public function onSearchQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
{ {
$note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id') $note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id');
->leftJoin(ActorCircle::class, 'actor_circle', Expr\Join::WITH, 'note_actor.id = actor_circle.tagged'); $actor_qb->leftJoin(ActorTag::class, 'actor_tag', Expr\Join::WITH, 'actor_tag.tagger = actor.id');
$actor_qb->leftJoin(ActorTag::class, 'actor_tag', Expr\Join::WITH, 'actor_tag.tagger = actor.id')
->leftJoin(ActorCircle::class, 'actor_circle', Expr\Join::WITH, 'actor.id = actor_circle.tagged');
return Event::next; return Event::next;
} }
public function onPostingAddFormEntries(Request $request, Actor $actor, array &$form_params) public function onPostingAddFormEntries(Request $request, Actor $actor, array &$form_params): bool
{ {
$form_params[] = ['tag_use_canonical', CheckboxType::class, ['required' => false, 'data' => true, 'label' => _m('Make note tags canonical'), 'help' => _m('Canonical tags will be treated as a version of an existing tag with the same root/stem (e.g. \'#great_tag\' will be considered as a version of \'#great\', if it already exists)')]]; $form_params[] = ['tag_use_canonical', CheckboxType::class, ['required' => false, 'data' => true, 'label' => _m('Make note tags canonical'), 'help' => _m('Canonical tags will be treated as a version of an existing tag with the same root/stem (e.g. \'#great_tag\' will be considered as a version of \'#great\', if it already exists)')]];
return Event::next; return Event::next;
} }
public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args) public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args): bool
{ {
if (!isset($data['tag_use_canonical'])) { if (!isset($data['tag_use_canonical'])) {
throw new ClientException; throw new ClientException(_m('Missing Use Canonical preference for Tags.'));
} }
$extra_args['tag_use_canonical'] = $data['tag_use_canonical']; $extra_args['tag_use_canonical'] = $data['tag_use_canonical'];
return Event::next; return Event::next;
} }
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs)
{
if ($section === 'profile' && $request->get('_route') === 'settings') {
$tabs[] = [
'title' => 'Self tags',
'desc' => 'Add or remove tags on yourself',
'id' => 'settings-self-tags',
'controller' => C\Tag::settingsSelfTags($request, Common::actor(), 'settings-self-tags-details'),
];
}
return Event::next;
}
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets)
{
$actor_id = $actor->getId();
$tags = Cache::get(
"actor-circle-{$actor_id}",
fn () => DB::dql('select c.tag from actor_circle c where c.tagger = :tagger', ['tagger' => $actor_id]),
);
foreach ($tags as $t) {
$t = '#' . $t['tag'];
$targets[$t] = $t;
}
return Event::next;
}
} }

View File

@ -44,7 +44,6 @@ use App\Core\Log;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Core\VisibilityScope; use App\Core\VisibilityScope;
use App\Entity\Note as GSNote; use App\Entity\Note as GSNote;
use App\Entity\NoteTag;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\DuplicateFoundException;
@ -57,6 +56,7 @@ use Component\Attachment\Entity\AttachmentToNote;
use Component\Conversation\Conversation; use Component\Conversation\Conversation;
use Component\FreeNetwork\FreeNetwork; use Component\FreeNetwork\FreeNetwork;
use Component\Language\Entity\Language; use Component\Language\Entity\Language;
use Component\Tag\Entity\NoteTag;
use Component\Tag\Tag; use Component\Tag\Tag;
use DateTime; use DateTime;
use DateTimeInterface; use DateTimeInterface;
@ -254,7 +254,7 @@ class Note extends Model
break; break;
case 'Hashtag': case 'Hashtag':
$match = ltrim($ap_tag->get('name'), '#'); $match = ltrim($ap_tag->get('name'), '#');
$tag = Tag::ensureValid($match); $tag = Tag::extract($match);
$canonical_tag = $ap_tag->get('canonical') ?? Tag::canonicalTag($tag, \is_null($lang_id = $obj->getLanguageId()) ? null : Language::getById($lang_id)->getLocale()); $canonical_tag = $ap_tag->get('canonical') ?? Tag::canonicalTag($tag, \is_null($lang_id = $obj->getLanguageId()) ? null : Language::getById($lang_id)->getLocale());
DB::persist(NoteTag::create([ DB::persist(NoteTag::create([
'tag' => $tag, 'tag' => $tag,

View File

@ -25,9 +25,8 @@ use App\Core\Cache;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use App\Entity\ActorTag; use Component\Circle\Entity\ActorTag;
use App\Entity\NoteTag; use Component\Tag\Entity\NoteTag;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class RelatedTags extends Plugin class RelatedTags extends Plugin
@ -37,21 +36,25 @@ class RelatedTags extends Plugin
*/ */
public function onAddPinnedFeedContent(Request $request, array &$pinned) public function onAddPinnedFeedContent(Request $request, array &$pinned)
{ {
$tags = $request->attributes->get('canons'); // Lets not use language, probably wouldn't make it more helpful
$tags = !\is_null($tags) ? explode(',', $tags) : [$request->attributes->get('canon')]; //$locale = $request->attributes->get('locale');
//$language_id = !empty($locale) ? Language::getByLocale($locale)->getId() : Common::actor()->getTopLanguage()->getId();
$tags = $request->attributes->get('tags');
$tags = !\is_null($tags) ? explode(',', $tags) : [$request->attributes->get('tag')];
switch ($request->attributes->get('_route')) { switch ($request->attributes->get('_route')) {
case 'single_note_tag': case 'single_note_tag':
// fall-through // fall-through
case 'multi_note_tags': case 'multi_note_tags':
$related = Cache::getList( $related = Cache::getList(
//"related-note-tags-{$language_id}-" . implode('-', $tags),
'related-note-tags-' . implode('-', $tags), 'related-note-tags-' . implode('-', $tags),
fn () => DB::sql( fn () => DB::sql(
<<<'EOQ' <<<'EOQ'
select distinct on (nt.canonical) canonical, nt.tag, nt.note_id, nt.created select distinct on (nt.canonical) canonical, nt.tag, nt.note_id, nt.canonical, nt.use_canonical, nt.created
from note_tag nt from note_tag nt
where nt.note_id in (select n.id from note n join note_tag nt on n.id = nt.note_id where nt.canonical in (:tags)) where nt.note_id in (select n.id from note n join note_tag nt on n.id = nt.note_id where nt.tag in (:tags))
and not nt.canonical in (:tags) and not nt.tag in (:tags)
limit 5 limit 5
EOQ, EOQ,
['tags' => $tags], ['tags' => $tags],
@ -68,10 +71,10 @@ class RelatedTags extends Plugin
'related-actor-tags-' . implode('-', $tags), 'related-actor-tags-' . implode('-', $tags),
fn () => DB::sql( fn () => DB::sql(
<<<'EOQ' <<<'EOQ'
select distinct on (at.canonical) canonical, at.tagger, at.tagged, at.tag, at.use_canonical, at.modified select distinct on (at.tag) tag, at.tagger, at.tagged, at.tag, at.modified
from actor_tag at from actor_tag at
where at.tagged in (select at.tagged from actor_tag at where at.canonical in (:tags)) where at.tagged in (select at.tagged from actor_tag at where at.tag in (:tags))
and not at.canonical in (:tags) and not at.tag in (:tags)
limit 5 limit 5
EOQ, EOQ,
['tags' => $tags], ['tags' => $tags],

View File

@ -29,14 +29,12 @@ use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\ActorTag;
use App\Entity\ActorTagBlock;
use App\Entity\Note; use App\Entity\Note;
use App\Entity\NoteTag;
use App\Entity\NoteTagBlock;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use Component\Language\Entity\Language; use Component\Language\Entity\Language;
use Component\Tag\Entity\NoteTag;
use Component\Tag\Entity\NoteTagBlock;
use Component\Tag\Tag; use Component\Tag\Tag;
use Functional as F; use Functional as F;
use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin; use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin;
@ -67,10 +65,10 @@ class AddBlocked extends Controller
$form_definition = []; $form_definition = [];
foreach ($blockable_tags as $nt) { foreach ($blockable_tags as $nt) {
$canon = $nt->getCanonical(); $canonical = $nt->getCanonical();
$form_definition[] = ["{$canon}:tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']]; $form_definition[] = ["{$canonical}:tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']];
$form_definition[] = ["{$canon}:use-canon", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]]; $form_definition[] = ["{$canonical}:use-canonical", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]];
$form_definition[] = ["{$canon}:add", SubmitType::class, ['label' => _m('Block')]]; $form_definition[] = ["{$canonical}:add", SubmitType::class, ['label' => _m('Block')]];
} }
$form = null; $form = null;
@ -80,24 +78,24 @@ class AddBlocked extends Controller
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData(); $data = $form->getData();
foreach ($form_definition as [$id, $_, $opts]) { foreach ($form_definition as [$id, $_, $opts]) {
[$canon, $type] = explode(':', $id); [$canonical, $type] = explode(':', $id);
if ($type === 'add') { if ($type === 'add') {
/** @var SubmitButton $button */ /** @var SubmitButton $button */
$button = $form->get($id); $button = $form->get($id);
if ($button->isClicked()) { if ($button->isClicked()) {
Cache::delete($block_class::cacheKey($user->getId())); Cache::delete($block_class::cacheKey($user->getId()));
Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]);
$new_tag = Tag::ensureValid($data[$canon . ':tag']); $new_tag = Tag::sanitize($data[$canonical . ':tag']);
$language = $target instanceof Note ? Language::getByNote($target)->getLocale() : $user->getActor()->getTopLanguage()->getLocale(); $language = $target instanceof Note ? Language::getByNote($target)->getLocale() : $user->getActor()->getTopLanguage()->getLocale();
$canonical_tag = Tag::canonicalTag($new_tag, $language); $canonical_tag = Tag::canonicalTag($new_tag, $language);
DB::persist($block_class::create([ DB::persist($block_class::create([
'blocker' => $user->getId(), 'blocker' => $user->getId(),
'tag' => $new_tag, 'tag' => $new_tag,
'canonical' => $canonical_tag, 'canonical' => $canonical_tag,
'use_canonical' => $data[$canon . ':use-canon'], 'use_canonical' => $data[$canonical . ':use-canonical'],
])); ]));
DB::flush(); DB::flush();
throw new RedirectException; throw new RedirectException();
} }
} }
} }
@ -131,20 +129,4 @@ class AddBlocked extends Controller
block_class: NoteTagBlock::class, block_class: NoteTagBlock::class,
); );
} }
public function addBlockedActorTags(Request $request, int $actor_id)
{
return self::addBlocked(
request: $request,
type_name: 'actor',
calculate_target: fn () => Actor::getById($actor_id),
calculate_blocks: fn ($user) => ActorTagBlock::getByActorId($user->getId()),
calculate_tags: fn ($blocks) => F\reject(
ActorTag::getByActorId($actor_id),
fn (ActorTag $nt) => ActorTagBlock::checkBlocksActorTag($nt, $blocks),
),
label: _m('Tags of the account above:'),
block_class: ActorTagBlock::class,
);
}
} }

View File

@ -28,10 +28,9 @@ use App\Core\Controller;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Entity\ActorTagBlock;
use App\Entity\NoteTagBlock;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use Component\Tag\Entity\NoteTagBlock;
use Component\Tag\Tag; use Component\Tag\Tag;
use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin; use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@ -105,7 +104,7 @@ class EditBlocked extends Controller
$data = $add_block_form->getData(); $data = $add_block_form->getData();
Cache::delete($block_class::cacheKey($user->getId())); Cache::delete($block_class::cacheKey($user->getId()));
Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]); Cache::delete(TagFilerPlugin::cacheKeys($user->getId())[$type_name]);
$new_tag = Tag::ensureValid($data['tag']); $new_tag = Tag::sanitize($data['tag']);
$language = $user->getActor()->getTopLanguage()->getLocale(); $language = $user->getActor()->getTopLanguage()->getLocale();
$canonical_tag = Tag::canonicalTag($new_tag, $language); $canonical_tag = Tag::canonicalTag($new_tag, $language);
DB::persist($block_class::create([ DB::persist($block_class::create([
@ -137,15 +136,4 @@ class EditBlocked extends Controller
block_class: NoteTagBlock::class, block_class: NoteTagBlock::class,
); );
} }
public static function editBlockedActorTags(Request $request)
{
return self::editBlocked(
request: $request,
type_name: 'actor',
calculate_blocks: fn ($user) => ActorTagBlock::getByActorId($user->getId()),
label: _m('Add blocked people tag:'),
block_class: ActorTagBlock::class,
);
}
} }

View File

@ -31,12 +31,10 @@ use App\Core\Modules\Plugin;
use App\Core\Router\RouteLoader; use App\Core\Router\RouteLoader;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\ActorTag;
use App\Entity\ActorTagBlock;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Entity\Note; use App\Entity\Note;
use App\Entity\NoteTag; use Component\Tag\Entity\NoteTag;
use App\Entity\NoteTagBlock; use Component\Tag\Entity\NoteTagBlock;
use Functional as F; use Functional as F;
use Plugin\TagBasedFiltering\Controller as C; use Plugin\TagBasedFiltering\Controller as C;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -88,10 +86,6 @@ class TagBasedFiltering extends Plugin
self::cacheKeys($actor)['note'], self::cacheKeys($actor)['note'],
fn () => DB::dql('select ntb from note_tag_block ntb where ntb.blocker = :blocker', ['blocker' => $actor->getId()]), fn () => DB::dql('select ntb from note_tag_block ntb where ntb.blocker = :blocker', ['blocker' => $actor->getId()]),
); );
$blocked_actor_tags = Cache::get(
self::cacheKeys($actor)['actor'],
fn () => DB::dql('select atb from actor_tag_block atb where atb.blocker = :blocker', ['blocker' => $actor->getId()]),
);
$notes = F\reject( $notes = F\reject(
$notes, $notes,
@ -102,10 +96,6 @@ class TagBasedFiltering extends Plugin
NoteTag::getByNoteId($n->getId()), NoteTag::getByNoteId($n->getId()),
fn ($nt) => NoteTagBlock::checkBlocksNoteTag($nt, $blocked_note_tags), fn ($nt) => NoteTagBlock::checkBlocksNoteTag($nt, $blocked_note_tags),
) )
|| F\some(
ActorTag::getByActorId($n->getActor()->getId()),
fn ($at) => ActorTagBlock::checkBlocksActorTag($at, $blocked_actor_tags),
)
) )
), ),
); );
@ -122,12 +112,6 @@ class TagBasedFiltering extends Plugin
'id' => 'settings-muting-note-tags', 'id' => 'settings-muting-note-tags',
'controller' => C\EditBlocked::editBlockedNoteTags($request), 'controller' => C\EditBlocked::editBlockedNoteTags($request),
]; ];
$tabs[] = [
'title' => 'Muted people tags',
'desc' => 'Edit your muted people tags',
'id' => 'settings-muting-actor-tags',
'controller' => C\EditBlocked::editBlockedActorTags($request),
];
} }
return Event::next; return Event::next;
} }

View File

@ -142,12 +142,6 @@ class Activity extends Entity
return DB::findOneBy($this->getObjectType(), ['id' => $this->getObjectId()]); return DB::findOneBy($this->getObjectType(), ['id' => $this->getObjectId()]);
} }
public function getNotificationTargetIdsFromActorTags(): array
{
$actor_circles = $this->getActor()->getActorCircles();
return F\flat_map($actor_circles, fn ($circle) => $circle->getSubscribedActors());
}
/** /**
* Who should be notified about this object? * Who should be notified about this object?
* *
@ -157,13 +151,6 @@ class Activity extends Entity
{ {
$target_ids = []; $target_ids = [];
// Actor Circles
if (\array_key_exists('actor_circle', $ids_already_known)) {
array_push($target_ids, ...$ids_already_known['actor_circle']);
} else {
array_push($target_ids, ...$this->getNotificationTargetIdsFromActorTags());
}
// Notifications // Notifications
if (\array_key_exists('notification_activity', $ids_already_known)) { if (\array_key_exists('notification_activity', $ids_already_known)) {
array_push($target_ids, ...$ids_already_known['notification_activity']); array_push($target_ids, ...$ids_already_known['notification_activity']);

View File

@ -257,7 +257,7 @@ class Actor extends Entity
'id' => "actor-id-{$actor_id}", 'id' => "actor-id-{$actor_id}",
'nickname' => "actor-nickname-id-{$actor_id}", 'nickname' => "actor-nickname-id-{$actor_id}",
'fullname' => "actor-fullname-id-{$actor_id}", 'fullname' => "actor-fullname-id-{$actor_id}",
'tags' => \is_null($other) ? "actor-tags-{$actor_id}" : "actor-tags-{$actor_id}-by-{$other}", // $other is $context_id 'self-tags' => "actor-self-tags-{$actor_id}",
'circles' => "actor-circles-{$actor_id}", 'circles' => "actor-circles-{$actor_id}",
'subscriber' => "subscriber-{$actor_id}", 'subscriber' => "subscriber-{$actor_id}",
'subscribed' => "subscribed-{$actor_id}", 'subscribed' => "subscribed-{$actor_id}",
@ -309,62 +309,20 @@ class Actor extends Entity
} }
/** /**
* Tags attributed to self, shortcut function for increased legibility * @return array ActorTag[] Self Tag Circles of which this actor is a member
*
* @return ActorTag[] resulting lists
*/ */
public function getSelfTags(bool $_test_force_recompute = false): array public function getSelfTags(): array
{ {
return $this->getOtherTags(context: $this->getId(), _test_force_recompute: $_test_force_recompute); return Cache::getList(
self::cacheKeys($this->getId())['self-tags'],
fn() => DB::findBy('actor_tag', ['tagger' => $this->getId(), 'tagged' => $this->getId()], order_by: ['modified' => 'DESC']),
);
} }
/** /**
* Get tags that other people put on this actor, in reverse-chron order * @return array ActorCircle[]
*
* @param null|Actor|int $context Actor we are requesting as:
* - If null = All tags attributed to self by other actors (excludes self tags)
* - If self = Same as getSelfTags
* - otherwise = Tags that $context attributed to $this
* @param null|int $offset Offset from latest
* @param null|int $limit Max number to get
*
* @return ActorTag[] resulting lists
*/ */
public function getOtherTags(self|int|null $context = null, ?int $offset = null, ?int $limit = null, bool $_test_force_recompute = false): array public function getCircles(): array
{
if (\is_null($context)) {
return Cache::getList(
self::cacheKeys($this->getId())['tags'],
fn() => DB::dql(
<<< 'EOQ'
SELECT tag
FROM actor_tag tag
WHERE tag.tagged = :id
ORDER BY tag.modified DESC, tag.tagged DESC
EOQ,
['id' => $this->getId()],
options: ['offset' => $offset, 'limit' => $limit],
),
);
} else {
$context_id = \is_int($context) ? $context : $context->getId();
return Cache::getList(
self::cacheKeys($this->getId(), $context_id)['tags'],
fn() => DB::dql(
<<< 'EOQ'
SELECT tag
FROM actor_tag tag
WHERE tag.tagged = :tagged_id AND tag.tagger = :tagger_id
ORDER BY tag.modified DESC, tag.tagged DESC
EOQ,
['tagged_id' => $this->getId(), 'tagger_id' => $context_id],
options: ['offset' => $offset, 'limit' => $limit],
),
);
}
}
public function getActorCircles()
{ {
return Cache::getList( return Cache::getList(
self::cacheKeys($this->getId())['circles'], self::cacheKeys($this->getId())['circles'],
@ -410,6 +368,24 @@ class Actor extends Entity
EOF, ['self' => $this->getId()]); EOF, ['self' => $this->getId()]);
} }
public function getSubscriptionsUrl(): string
{
if ($this->getIsLocal()) {
return Router::url('actor_subscriptions_nickname', ['nickname' => $this->getNickname()]);
} else {
return Router::url('actor_subscriptions_id', ['id' => $this->getId()]);
}
}
public function getSubscribersUrl(): string
{
if ($this->getIsLocal()) {
return Router::url('actor_subscribers_nickname', ['nickname' => $this->getNickname()]);
} else {
return Router::url('actor_subscribers_id', ['id' => $this->getId()]);
}
}
/** /**
* Resolve an ambiguous nickname reference, checking in following order: * Resolve an ambiguous nickname reference, checking in following order:
* - Actors that $sender subscribes to * - Actors that $sender subscribes to
@ -428,9 +404,9 @@ class Actor extends Entity
self::cacheKeys($this->getId(), $nickname)['relative-nickname'], self::cacheKeys($this->getId(), $nickname)['relative-nickname'],
fn () => DB::dql( fn () => DB::dql(
<<<'EOF' <<<'EOF'
select a from actor a where SELECT a FROM actor AS a WHERE
a.id in (select fa.subscribed_id from subscription fa join actor aa with fa.subscribed = aa.id where fa.subscriber = :actor_id and aa.nickname = :nickname) or a.id IN (SELECT sa.subscribed_id FROM subscription sa JOIN actor aa WITH sa.subscribed_id = aa.id WHERE sa.subscriber_id = :actor_id AND aa.nickname = :nickname) OR
a.id in (select fb.subscriber_id from subscription fb join actor ab with fb.subscriber = ab.id where fb.subscribed = :actor_id and ab.nickname = :nickname) or a.id IN (SELECT sb.subscriber_id FROM subscription sb JOIN actor ab WITH sb.subscriber_id = ab.id WHERE sb.subscribed_id = :actor_id AND ab.nickname = :nickname) OR
a.nickname = :nickname a.nickname = :nickname
EOF, EOF,
['nickname' => $nickname, 'actor_id' => $this->getId()], ['nickname' => $nickname, 'actor_id' => $this->getId()],