diff --git a/components/Notification/Notification.php b/components/Notification/Notification.php index 371f036b40..9f19507efc 100644 --- a/components/Notification/Notification.php +++ b/components/Notification/Notification.php @@ -60,7 +60,7 @@ class Notification extends Component */ public function onNewNotification(Actor $sender, Activity $activity, array $ids_already_known = [], ?string $reason = null): bool { - $targets = $activity->getNotificationTargets($ids_already_known, sender_id: $sender->getId()); + $targets = $activity->getNotificationTargets(ids_already_known: $ids_already_known, sender_id: $sender->getId()); $this->notify($sender, $activity, $targets, $reason); return Event::next; @@ -83,8 +83,16 @@ class Notification extends Component } } // TODO: use https://symfony.com/doc/current/notifier.html + DB::persist(Entity\Notification::create([ + 'activity_id' => $activity->getId(), + 'target_id' => $target->getId(), + 'reason' => $reason, + ])); } else { - $remote_targets[] = $target; + // We have no authority nor responsibility of notifying remote actors of a remote actor's doing + if ($sender->getIsLocal()) { + $remote_targets[] = $target; + } } } diff --git a/components/Tag/Tag.php b/components/Tag/Tag.php index 043aabdaf5..658ca75fd8 100644 --- a/components/Tag/Tag.php +++ b/components/Tag/Tag.php @@ -73,9 +73,13 @@ class Tag extends Component */ public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): bool { - $matched_tags = []; + if ($extra_args['TagProcessed'] ?? false) { + return Event::next; + } // XXX: We remove because when content is in html the tag comes as #hashtag - preg_match_all(self::TAG_REGEX, str_replace('', '', $content), $matched_tags, \PREG_SET_ORDER); + $content = str_replace('', '', $content); + $matched_tags = []; + 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); diff --git a/plugins/ActivityPub/Controller/Inbox.php b/plugins/ActivityPub/Controller/Inbox.php index bee8f59184..5aa91f23e4 100644 --- a/plugins/ActivityPub/Controller/Inbox.php +++ b/plugins/ActivityPub/Controller/Inbox.php @@ -34,6 +34,7 @@ namespace Plugin\ActivityPub\Controller; use App\Core\Controller; use App\Core\DB\DB; +use App\Core\Event; use function App\Core\I18n\_m; use App\Core\Log; use App\Core\Router\Router; @@ -164,7 +165,9 @@ class Inbox extends Controller $ap_actor->getActorId(), Discovery::normalize($actor->getNickname() . '@' . parse_url($ap_actor->getInboxUri(), PHP_URL_HOST)), ); + Event::handle('NewNotification', [$actor, $ap_act->getActivity(), [], "{$actor->getNickname()} mentioned you in a note"]); DB::flush(); + dd($ap_act, $act = $ap_act->getActivity(), $act->getActor(), $act->getObject()); return new TypeResponse($type, status: 202); diff --git a/plugins/ActivityPub/Util/Model/Note.php b/plugins/ActivityPub/Util/Model/Note.php index aa79b906e6..c5688c2d07 100644 --- a/plugins/ActivityPub/Util/Model/Note.php +++ b/plugins/ActivityPub/Util/Model/Note.php @@ -34,6 +34,7 @@ namespace Plugin\ActivityPub\Util\Model; use ActivityPhp\Type; use ActivityPhp\Type\AbstractObject; +use App\Core\Cache; use App\Core\DB\DB; use App\Core\Event; use App\Core\GSFile; @@ -43,6 +44,7 @@ use App\Core\Log; use App\Core\Router\Router; use App\Entity\Language; use App\Entity\Note as GSNote; +use App\Entity\NoteTag; use App\Util\Common; use App\Util\Exception\ClientException; use App\Util\Exception\DuplicateFoundException; @@ -53,6 +55,7 @@ use App\Util\TemporaryFile; use Component\Attachment\Entity\ActorToAttachment; use Component\Attachment\Entity\AttachmentToNote; use Component\Conversation\Conversation; +use Component\Tag\Tag; use DateTime; use DateTimeInterface; use Exception; @@ -200,8 +203,39 @@ class Note extends Model // Assign conversation to this note Conversation::assignLocalConversation($obj, $reply_to); - // Need file and note ids for the next step - Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = []]); + $object_mentions_ids = []; + foreach ($type_note->get('tag') as $ap_tag) { + switch ($ap_tag->get('type')) { + case 'Mention': + try { + $actor = ActivityPub::getActorByUri($ap_tag->get('href')); + if ($actor->getIsLocal()) { + $object_mentions_ids[] = $actor->getId(); + } + } catch (Exception $e) { + Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); + } + break; + case 'Hashtag': + $match = ltrim($ap_tag->get('name'), '#'); + $tag = Tag::ensureValid($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, + 'canonical' => $canonical_tag, + 'note_id' => $obj->getId(), + 'use_canonical' => $ap_tag->get('canonical') ?? false, + ])); + Cache::pushList("tag-{$canonical_tag}", $obj); + foreach (Tag::cacheKeys($canonical_tag) as $key) { + Cache::delete($key); + } + break; + } + } + $obj->setObjectMentionsIds($object_mentions_ids); + // The content would be non-sanitized text/html + Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = ['TagProcessed' => true]]); if ($processed_attachments !== []) { foreach ($processed_attachments as [$a, $fname]) { @@ -268,9 +302,10 @@ class Note extends Model // Hashtags foreach ($object->getTags() as $hashtag) { $attr['tag'][] = [ - 'type' => 'Hashtag', - 'href' => $hashtag->getUrl(type: Router::ABSOLUTE_URL), - 'name' => "#{$hashtag->getTag()}", + 'type' => 'Hashtag', + 'href' => $hashtag->getUrl(type: Router::ABSOLUTE_URL), + 'name' => "#{$hashtag->getTag()}", + 'canonical' => $hashtag->getCanonical(), ]; } diff --git a/src/Entity/Note.php b/src/Entity/Note.php index bad754ebdb..5da7be4e67 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -380,15 +380,25 @@ class Note extends Entity /** * @return array of ids of Actors */ + private array $object_mentions_ids = []; + public function setObjectMentionsIds(array $mentions): self + { + $this->object_mentions_ids = $mentions; + return $this; + } public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null): array { - $target_ids = []; - if (!\array_key_exists('object', $ids_already_known)) { - $mentions = Formatting::findMentions($this->getContent(), $this->getActor()); - foreach ($mentions as $mention) { - foreach ($mention['mentioned'] as $m) { - $target_ids[] = $m->getId(); + $target_ids = $this->object_mentions_ids ?? []; + if ($target_ids === []) { + if (!\array_key_exists('object', $ids_already_known)) { + $mentions = Formatting::findMentions($this->getContent(), $this->getActor()); + foreach ($mentions as $mention) { + foreach ($mention['mentioned'] as $m) { + $target_ids[] = $m->getId(); + } } + } else { + $target_ids = $ids_already_known['object']; } } diff --git a/src/Util/Formatting.php b/src/Util/Formatting.php index 7de2292e2b..b5d08180ac 100644 --- a/src/Util/Formatting.php +++ b/src/Util/Formatting.php @@ -263,7 +263,7 @@ abstract class Formatting // php-intl is highly recommended... if (!\function_exists('transliterator_transliterate')) { $str = preg_replace('/[^\pL\pN]/u', '', $str); - $str = mb_convert_case($str, \MB_CASE_LOWER, 'UTF-8'); + $str = mb_convert_case($str, MB_CASE_LOWER, 'UTF-8'); return mb_substr($str, 0, $length); } $str = transliterator_transliterate('Any-Latin;' // any charset to latin compatible @@ -290,6 +290,8 @@ abstract class Formatting public static function findMentions(string $text, Actor $actor): array { $mentions = []; + // XXX: We remove because when content is in html the tag comes as #hashtag + $text = str_replace('', '', $text); if (Event::handle('StartFindMentions', [$actor, $text, &$mentions])) { $matches = self::findMentionsRaw($text, '@');