diff --git a/components/Search/Controller/Search.php b/components/Search/Controller/Search.php
index 13c7a8f8cc..0fa2adf3fc 100644
--- a/components/Search/Controller/Search.php
+++ b/components/Search/Controller/Search.php
@@ -48,7 +48,7 @@ class Search extends FeedController
$language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null;
$q = $this->string('q');
- $data = $this->query(query: $q, language: $language);
+ $data = $this->query(query: $q, locale: $language);
$notes = $data['notes'];
$actors = $data['actors'];
diff --git a/components/Tag/Controller/Tag.php b/components/Tag/Controller/Tag.php
index 49c52224ac..b93f9d211b 100644
--- a/components/Tag/Controller/Tag.php
+++ b/components/Tag/Controller/Tag.php
@@ -6,31 +6,34 @@ namespace Component\Tag\Controller;
use App\Core\Cache;
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\Exception\BugFoundException;
-use App\Util\Exception\ClientException;
-use App\Util\Exception\RedirectException;
-use App\Util\Formatting;
-use Component\Tag\Form\SelfTagsForm;
+use Component\Language\Entity\Language;
use Component\Tag\Tag as CompTag;
-use Symfony\Component\Form\SubmitButton;
-use Symfony\Component\HttpFoundation\Request;
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();
$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(
key: $key,
query: $query,
- query_args: ['canon' => $canon_single_or_multi],
+ query_args: $query_args,
actor: $actor,
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(
- canon_single_or_multi: $canon,
- tag_single_or_multi: $this->string('tag'),
- key: CompTag::cacheKeys($canon)['note_single'],
- 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',
+ tag_single_or_multi: $tag,
+ key: CompTag::cacheKeys($tag)['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',
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(
- 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))['note_multi'],
- 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',
+ tag_single_or_multi: explode(',', $tags),
+ key: CompTag::cacheKeys(str_replace(',', '-', $tags))['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',
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(),
- ];
- }
}
diff --git a/src/Entity/NoteTag.php b/components/Tag/Entity/NoteTag.php
similarity index 80%
rename from src/Entity/NoteTag.php
rename to components/Tag/Entity/NoteTag.php
index a0260b81e9..01f4eb6ef9 100644
--- a/src/Entity/NoteTag.php
+++ b/components/Tag/Entity/NoteTag.php
@@ -19,12 +19,15 @@ declare(strict_types = 1);
// along with GNU social. If not, see .
// }}}
-namespace App\Entity;
+namespace Component\Tag\Entity;
use App\Core\Cache;
use App\Core\DB\DB;
use App\Core\Entity;
use App\Core\Router\Router;
+use App\Entity\Actor;
+use App\Entity\Note;
+use Component\Language\Entity\Language;
use Component\Tag\Tag;
use DateTimeInterface;
@@ -39,6 +42,7 @@ use DateTimeInterface;
* @author Mikael Nordfeldth
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
* @author Hugo Sales
+ * @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
* @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 bool $use_canonical;
private ?int $language_id = null;
- private \DateTimeInterface $created;
+ private DateTimeInterface $created;
public function setTag(string $tag): self
{
- $this->tag = \mb_substr($tag, 0, 64);
+ $this->tag = mb_substr($tag, 0, 64);
return $this;
}
@@ -66,7 +70,7 @@ class NoteTag extends Entity
public function setCanonical(string $canonical): self
{
- $this->canonical = \mb_substr($canonical, 0, 64);
+ $this->canonical = mb_substr($canonical, 0, 64);
return $this;
}
@@ -108,13 +112,13 @@ class NoteTag extends Entity
return $this->language_id;
}
- public function setCreated(\DateTimeInterface $created): self
+ public function setCreated(DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
- public function getCreated(): \DateTimeInterface
+ public function getCreated(): DateTimeInterface
{
return $this->created;
}
@@ -132,15 +136,24 @@ class NoteTag extends Entity
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
{
- $params = ['canon' => $this->getCanonical(), 'tag' => $this->getTag()];
- if (!\is_null($actor)) {
- $params['lang'] = $actor->getTopLanguage()->getLocale();
+ $params['tag'] = $this->getTag();
+
+ 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);
}
@@ -150,18 +163,18 @@ class NoteTag extends Entity
'name' => 'note_tag',
'description' => 'Hash tags on notes',
'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'],
'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'],
'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'],
],
- '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' => [
'note_tag_created_idx' => ['created'],
'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'],
],
];
diff --git a/src/Entity/NoteTagBlock.php b/components/Tag/Entity/NoteTagBlock.php
similarity index 93%
rename from src/Entity/NoteTagBlock.php
rename to components/Tag/Entity/NoteTagBlock.php
index dc0c195af6..7daf15abdf 100644
--- a/src/Entity/NoteTagBlock.php
+++ b/components/Tag/Entity/NoteTagBlock.php
@@ -19,7 +19,7 @@ declare(strict_types = 1);
// along with GNU social. If not, see .
// }}}
-namespace App\Entity;
+namespace Component\Tag\Entity;
use App\Core\Cache;
use App\Core\DB\DB;
@@ -46,7 +46,7 @@ class NoteTagBlock extends Entity
private string $tag;
private string $canonical;
private bool $use_canonical;
- private \DateTimeInterface $modified;
+ private DateTimeInterface $modified;
public function setBlocker(int $blocker): self
{
@@ -61,7 +61,7 @@ class NoteTagBlock extends Entity
public function setTag(string $tag): self
{
- $this->tag = \mb_substr($tag, 0, 64);
+ $this->tag = mb_substr($tag, 0, 64);
return $this;
}
@@ -72,7 +72,7 @@ class NoteTagBlock extends Entity
public function setCanonical(string $canonical): self
{
- $this->canonical = \mb_substr($canonical, 0, 64);
+ $this->canonical = mb_substr($canonical, 0, 64);
return $this;
}
@@ -92,13 +92,13 @@ class NoteTagBlock extends Entity
return $this->use_canonical;
}
- public function setModified(\DateTimeInterface $modified): self
+ public function setModified(DateTimeInterface $modified): self
{
$this->modified = $modified;
return $this;
}
- public function getModified(): \DateTimeInterface
+ public function getModified(): DateTimeInterface
{
return $this->modified;
}
diff --git a/components/Tag/Form/SelfTagsForm.php b/components/Tag/Form/SelfTagsForm.php
deleted file mode 100644
index 499becf8bc..0000000000
--- a/components/Tag/Form/SelfTagsForm.php
+++ /dev/null
@@ -1,63 +0,0 @@
-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];
- }
-}
diff --git a/components/Tag/Tag.php b/components/Tag/Tag.php
index ca654a4957..c21829cb62 100644
--- a/components/Tag/Tag.php
+++ b/components/Tag/Tag.php
@@ -30,18 +30,15 @@ use function App\Core\I18n\_m;
use App\Core\Modules\Component;
use App\Core\Router\Router;
use App\Entity\Actor;
-use App\Entity\ActorCircle;
-use App\Entity\ActorTag;
use App\Entity\Note;
-use App\Entity\NoteTag;
use App\Util\Common;
use App\Util\Exception\ClientException;
use App\Util\Formatting;
use App\Util\Functional as GSF;
use App\Util\HTML;
-use App\Util\Nickname;
+use Component\Circle\Entity\ActorTag;
use Component\Language\Entity\Language;
-use Component\Tag\Controller as C;
+use Component\Tag\Entity\NoteTag;
use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr;
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
*
* @author Hugo Sales
+ * @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class Tag extends Component
{
- 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_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/';
- public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,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_SLUG_REGEX = '[A-Za-z0-9]{1,64}';
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('multi_note_tags', '/note-tags/{canons<(' . 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']);
+ $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/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']);
return Event::next;
}
@@ -86,7 +81,10 @@ class Tag extends Component
preg_match_all(self::TAG_REGEX, $content, $matched_tags, \PREG_SET_ORDER);
$matched_tags = array_unique(F\map($matched_tags, fn ($m) => $m[2]));
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());
DB::persist(NoteTag::create([
'tag' => $tag,
@@ -103,38 +101,54 @@ class Tag extends Component
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;
}
- public static function cacheKeys(string $canon_single_or_multi): array
+ public static function cacheKeys(string $tag_single_or_multi): array
{
return [
- 'note_single' => "note-tag-feed-{$canon_single_or_multi}",
- 'note_multi' => "note-tags-feed-{$canon_single_or_multi}",
- 'actor_single' => "actor-tag-feed-{$canon_single_or_multi}",
- 'actor_multi' => "actor-tags-feed-{$canon_single_or_multi}",
+ 'note_single' => "note-tag-feed-{$tag_single_or_multi}",
+ 'note_multi' => "note-tags-feed-{$tag_single_or_multi}",
+ 'actor_single' => "actor-tag-feed-{$tag_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);
- $canonical = self::canonicalTag($tag, $language);
- $url = Router::url('single_note_tag', !\is_null($language) ? ['canon' => $canonical, 'lang' => $language, 'tag' => $tag] : ['canon' => $canonical, 'tag' => $tag]);
- return HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => $tag, 'rel' => 'tag'], $tag]], options: ['indent' => false]);
+ $tag = self::extract($tag);
+ $url = Router::url('single_note_tag', !\is_null($locale) ? ['tag' => $tag, 'locale' => $locale] : ['tag' => $tag]);
+ return HTML::html(['span' => ['attrs' => ['class' => 'tag'],
+ '#' . 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, '#'));
- if (preg_match(self::TAG_REGEX, '#' . $tag)) {
- return $tag;
- } else {
+ return self::ensureLength(Formatting::removePrefix($tag, '#'));
+ }
+
+ 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]));
}
+ return $tag;
}
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
* 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 = '';
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
*/
- 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, ':')) {
return Event::next;
}
+ if (\is_null($locale)) {
+ $locale = Common::currentLanguage();
+ }
[$search_type, $search_term] = explode(':', $term);
if (str_starts_with($search_term, '#')) {
- $search_term = self::ensureValid($search_term);
- $canon_search_term = self::canonicalTag($search_term, $language);
- $temp_note_expr = $eb->eq('note_tag.canonical', $canon_search_term);
- $temp_actor_expr = $eb->eq('actor_tag.canonical', $canon_search_term);
+ $search_term = self::sanitize($search_term);
+ $canonical_search_term = self::canonicalTag($search_term, $locale);
+ $temp_note_expr = $eb->eq('note_tag.canonical', $canonical_search_term);
+ $temp_actor_expr = $eb->eq('actor_tag.canonical', $canonical_search_term);
if (Formatting::startsWith($term, ['note:', 'tag:', 'people:'])) {
$note_expr = $temp_note_expr;
} 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: ['-', '_']))) {
$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()));
- $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);
$search_expr = $eb->andX(
$tagger_expr,
@@ -202,52 +219,23 @@ class Tag extends Component
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')
- ->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')
- ->leftJoin(ActorCircle::class, 'actor_circle', Expr\Join::WITH, 'actor.id = actor_circle.tagged');
+ $note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id');
+ $actor_qb->leftJoin(ActorTag::class, 'actor_tag', Expr\Join::WITH, 'actor_tag.tagger = actor.id');
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)')]];
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'])) {
- throw new ClientException;
+ throw new ClientException(_m('Missing Use Canonical preference for Tags.'));
}
$extra_args['tag_use_canonical'] = $data['tag_use_canonical'];
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;
- }
}
diff --git a/plugins/ActivityPub/Util/Model/Note.php b/plugins/ActivityPub/Util/Model/Note.php
index f2389d57c1..0ea3dfb367 100644
--- a/plugins/ActivityPub/Util/Model/Note.php
+++ b/plugins/ActivityPub/Util/Model/Note.php
@@ -44,7 +44,6 @@ use App\Core\Log;
use App\Core\Router\Router;
use App\Core\VisibilityScope;
use App\Entity\Note as GSNote;
-use App\Entity\NoteTag;
use App\Util\Common;
use App\Util\Exception\ClientException;
use App\Util\Exception\DuplicateFoundException;
@@ -57,6 +56,7 @@ use Component\Attachment\Entity\AttachmentToNote;
use Component\Conversation\Conversation;
use Component\FreeNetwork\FreeNetwork;
use Component\Language\Entity\Language;
+use Component\Tag\Entity\NoteTag;
use Component\Tag\Tag;
use DateTime;
use DateTimeInterface;
@@ -254,7 +254,7 @@ class Note extends Model
break;
case 'Hashtag':
$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());
DB::persist(NoteTag::create([
'tag' => $tag,
diff --git a/plugins/RelatedTags/RelatedTags.php b/plugins/RelatedTags/RelatedTags.php
index a70931ac98..9913ca6857 100644
--- a/plugins/RelatedTags/RelatedTags.php
+++ b/plugins/RelatedTags/RelatedTags.php
@@ -25,9 +25,8 @@ use App\Core\Cache;
use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Modules\Plugin;
-use App\Entity\ActorTag;
-use App\Entity\NoteTag;
-
+use Component\Circle\Entity\ActorTag;
+use Component\Tag\Entity\NoteTag;
use Symfony\Component\HttpFoundation\Request;
class RelatedTags extends Plugin
@@ -37,21 +36,25 @@ class RelatedTags extends Plugin
*/
public function onAddPinnedFeedContent(Request $request, array &$pinned)
{
- $tags = $request->attributes->get('canons');
- $tags = !\is_null($tags) ? explode(',', $tags) : [$request->attributes->get('canon')];
+ // Lets not use language, probably wouldn't make it more helpful
+ //$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')) {
case 'single_note_tag':
// fall-through
case 'multi_note_tags':
$related = Cache::getList(
+ //"related-note-tags-{$language_id}-" . implode('-', $tags),
'related-note-tags-' . implode('-', $tags),
fn () => DB::sql(
<<<'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
- 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))
- and not 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.tag in (:tags)
limit 5
EOQ,
['tags' => $tags],
@@ -68,10 +71,10 @@ class RelatedTags extends Plugin
'related-actor-tags-' . implode('-', $tags),
fn () => DB::sql(
<<<'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
- where at.tagged in (select at.tagged from actor_tag at where at.canonical in (:tags))
- and not at.canonical in (:tags)
+ where at.tagged in (select at.tagged from actor_tag at where at.tag in (:tags))
+ and not at.tag in (:tags)
limit 5
EOQ,
['tags' => $tags],
diff --git a/plugins/TagBasedFiltering/Controller/AddBlocked.php b/plugins/TagBasedFiltering/Controller/AddBlocked.php
index 6926a069dc..d50e8d480d 100644
--- a/plugins/TagBasedFiltering/Controller/AddBlocked.php
+++ b/plugins/TagBasedFiltering/Controller/AddBlocked.php
@@ -29,14 +29,12 @@ use App\Core\DB\DB;
use App\Core\Form;
use function App\Core\I18n\_m;
use App\Entity\Actor;
-use App\Entity\ActorTag;
-use App\Entity\ActorTagBlock;
use App\Entity\Note;
-use App\Entity\NoteTag;
-use App\Entity\NoteTagBlock;
use App\Util\Common;
use App\Util\Exception\RedirectException;
use Component\Language\Entity\Language;
+use Component\Tag\Entity\NoteTag;
+use Component\Tag\Entity\NoteTagBlock;
use Component\Tag\Tag;
use Functional as F;
use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin;
@@ -67,10 +65,10 @@ class AddBlocked extends Controller
$form_definition = [];
foreach ($blockable_tags as $nt) {
- $canon = $nt->getCanonical();
- $form_definition[] = ["{$canon}: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[] = ["{$canon}:add", SubmitType::class, ['label' => _m('Block')]];
+ $canonical = $nt->getCanonical();
+ $form_definition[] = ["{$canonical}:tag", TextType::class, ['data' => '#' . $nt->getTag(), 'label' => ' ']];
+ $form_definition[] = ["{$canonical}:use-canonical", CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Block all similar tags'), 'required' => false, 'data' => true]];
+ $form_definition[] = ["{$canonical}:add", SubmitType::class, ['label' => _m('Block')]];
}
$form = null;
@@ -80,24 +78,24 @@ class AddBlocked extends Controller
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
foreach ($form_definition as [$id, $_, $opts]) {
- [$canon, $type] = explode(':', $id);
+ [$canonical, $type] = explode(':', $id);
if ($type === 'add') {
/** @var SubmitButton $button */
$button = $form->get($id);
if ($button->isClicked()) {
Cache::delete($block_class::cacheKey($user->getId()));
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();
$canonical_tag = Tag::canonicalTag($new_tag, $language);
DB::persist($block_class::create([
'blocker' => $user->getId(),
'tag' => $new_tag,
'canonical' => $canonical_tag,
- 'use_canonical' => $data[$canon . ':use-canon'],
+ 'use_canonical' => $data[$canonical . ':use-canonical'],
]));
DB::flush();
- throw new RedirectException;
+ throw new RedirectException();
}
}
}
@@ -131,20 +129,4 @@ class AddBlocked extends Controller
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,
- );
- }
}
diff --git a/plugins/TagBasedFiltering/Controller/EditBlocked.php b/plugins/TagBasedFiltering/Controller/EditBlocked.php
index 5ab4162530..d0910d57ec 100644
--- a/plugins/TagBasedFiltering/Controller/EditBlocked.php
+++ b/plugins/TagBasedFiltering/Controller/EditBlocked.php
@@ -28,10 +28,9 @@ use App\Core\Controller;
use App\Core\DB\DB;
use App\Core\Form;
use function App\Core\I18n\_m;
-use App\Entity\ActorTagBlock;
-use App\Entity\NoteTagBlock;
use App\Util\Common;
use App\Util\Exception\RedirectException;
+use Component\Tag\Entity\NoteTagBlock;
use Component\Tag\Tag;
use Plugin\TagBasedFiltering\TagBasedFiltering as TagFilerPlugin;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -105,7 +104,7 @@ class EditBlocked extends Controller
$data = $add_block_form->getData();
Cache::delete($block_class::cacheKey($user->getId()));
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();
$canonical_tag = Tag::canonicalTag($new_tag, $language);
DB::persist($block_class::create([
@@ -137,15 +136,4 @@ class EditBlocked extends Controller
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,
- );
- }
}
diff --git a/plugins/TagBasedFiltering/TagBasedFiltering.php b/plugins/TagBasedFiltering/TagBasedFiltering.php
index 759da83ecd..af609d0677 100644
--- a/plugins/TagBasedFiltering/TagBasedFiltering.php
+++ b/plugins/TagBasedFiltering/TagBasedFiltering.php
@@ -31,12 +31,10 @@ use App\Core\Modules\Plugin;
use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Actor;
-use App\Entity\ActorTag;
-use App\Entity\ActorTagBlock;
use App\Entity\LocalUser;
use App\Entity\Note;
-use App\Entity\NoteTag;
-use App\Entity\NoteTagBlock;
+use Component\Tag\Entity\NoteTag;
+use Component\Tag\Entity\NoteTagBlock;
use Functional as F;
use Plugin\TagBasedFiltering\Controller as C;
use Symfony\Component\HttpFoundation\Request;
@@ -88,10 +86,6 @@ class TagBasedFiltering extends Plugin
self::cacheKeys($actor)['note'],
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,
@@ -102,10 +96,6 @@ class TagBasedFiltering extends Plugin
NoteTag::getByNoteId($n->getId()),
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',
'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;
}
diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php
index 1ddacc990a..ab9e157d7f 100644
--- a/src/Entity/Activity.php
+++ b/src/Entity/Activity.php
@@ -142,12 +142,6 @@ class Activity extends Entity
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?
*
@@ -157,13 +151,6 @@ class Activity extends Entity
{
$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
if (\array_key_exists('notification_activity', $ids_already_known)) {
array_push($target_ids, ...$ids_already_known['notification_activity']);
diff --git a/src/Entity/Actor.php b/src/Entity/Actor.php
index 58a31ebe35..9b821b5973 100644
--- a/src/Entity/Actor.php
+++ b/src/Entity/Actor.php
@@ -257,7 +257,7 @@ class Actor extends Entity
'id' => "actor-id-{$actor_id}",
'nickname' => "actor-nickname-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}",
'subscriber' => "subscriber-{$actor_id}",
'subscribed' => "subscribed-{$actor_id}",
@@ -309,62 +309,20 @@ class Actor extends Entity
}
/**
- * Tags attributed to self, shortcut function for increased legibility
- *
- * @return ActorTag[] resulting lists
+ * @return array ActorTag[] Self Tag Circles of which this actor is a member
*/
- 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
- *
- * @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
+ * @return array ActorCircle[]
*/
- public function getOtherTags(self|int|null $context = null, ?int $offset = null, ?int $limit = null, bool $_test_force_recompute = false): 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()
+ public function getCircles(): array
{
return Cache::getList(
self::cacheKeys($this->getId())['circles'],
@@ -410,6 +368,24 @@ class Actor extends Entity
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:
* - Actors that $sender subscribes to
@@ -428,9 +404,9 @@ class Actor extends Entity
self::cacheKeys($this->getId(), $nickname)['relative-nickname'],
fn () => DB::dql(
<<<'EOF'
- select a from actor 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 fb.subscriber_id from subscription fb join actor ab with fb.subscriber = ab.id where fb.subscribed = :actor_id and ab.nickname = :nickname) or
+ SELECT a FROM actor AS a WHERE
+ 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 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
EOF,
['nickname' => $nickname, 'actor_id' => $this->getId()],