[COMPONENT][Notification] Make logic more generic and robust
Fixed various bugs Some important concepts to bear in mind: * Notification: Associated with activities, won't be reconstructed together with objects, can be thought of as transient * Attention: Associated with objects, will be reconstructed with them, can be thought as persistent * Notifications and Attentions have no direct implications. * Mentions are a specific form of attentions in notes, leads to the creation of Attentions. Finally, Potential PHP issue detected and reported: https://github.com/php/php-src/issues/8199 `static::method()` from a non static context (such as a class method) calls `__call`, rather than the expected `__callStatic`. Can be fixed by using `(static fn() => static::method())()`, but the usage of the magic method is strictly unnecessary in this case.
This commit is contained in:
@@ -44,6 +44,7 @@ use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Exception\NotImplementedException;
|
||||
use DateTimeInterface;
|
||||
use InvalidArgumentException;
|
||||
use const JSON_UNESCAPED_SLASHES;
|
||||
use Plugin\ActivityPub\ActivityPub;
|
||||
use Plugin\ActivityPub\Entity\ActivitypubActivity;
|
||||
use Plugin\ActivityPub\Util\Explorer;
|
||||
@@ -145,7 +146,7 @@ class Activity extends Model
|
||||
*
|
||||
* @throws ClientException
|
||||
*/
|
||||
public static function toJson(mixed $object, int $options = \JSON_UNESCAPED_SLASHES): string
|
||||
public static function toJson(mixed $object, int $options = JSON_UNESCAPED_SLASHES): string
|
||||
{
|
||||
if ($object::class !== GSActivity::class) {
|
||||
throw new InvalidArgumentException('First argument type must be an Activity.');
|
||||
@@ -169,10 +170,16 @@ class Activity extends Model
|
||||
'actor' => $object->getActor()->getUri(Router::ABSOLUTE_URL),
|
||||
];
|
||||
|
||||
$attr['to'] = [];
|
||||
$attr['cc'] = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
foreach ($object->getAttentionTargets() as $target) {
|
||||
$attr['cc'][] = $target->getUri();
|
||||
}
|
||||
|
||||
// Get object or Tombstone
|
||||
try {
|
||||
$object = $object->getObject(); // Throws NotFoundException
|
||||
$attr['object'] = ($attr['type'] === 'Create') ? self::jsonToType(Model::toJson($object)) : ActivityPub::getUriByObject($object);
|
||||
$child = $object->getObject(); // Throws NotFoundException
|
||||
$attr['object'] = ($attr['type'] === 'Create') ? self::jsonToType(Model::toJson($child)) : ActivityPub::getUriByObject($child);
|
||||
} catch (NotFoundException) {
|
||||
// It seems this object was deleted, refer to it as a Tombstone
|
||||
$uri = match ($object->getObjectType()) {
|
||||
@@ -180,18 +187,7 @@ class Activity extends Model
|
||||
'actor' => Router::url('actor_view_id', ['id' => $object->getObjectId()], type: Router::ABSOLUTE_URL),
|
||||
default => throw new NotImplementedException(),
|
||||
};
|
||||
$attr['object'] = Type::create('Tombstone', [
|
||||
'id' => $uri,
|
||||
]);
|
||||
}
|
||||
|
||||
// If embedded non tombstone Object
|
||||
if (!\is_string($attr['object']) && $attr['object']->get('type') !== 'Tombstone') {
|
||||
// Little special case
|
||||
if ($attr['type'] === 'Create' && ($attr['object']->get('type') === 'Note' || $attr['object']->get('type') === 'Page')) {
|
||||
$attr['to'] = $attr['object']->get('to') ?? [];
|
||||
$attr['cc'] = $attr['object']->get('cc') ?? [];
|
||||
}
|
||||
$attr['object'] = Type::create('Tombstone', ['id' => $uri]);
|
||||
}
|
||||
|
||||
if (!\is_string($attr['object'])) {
|
||||
|
@@ -94,7 +94,6 @@ class ActivityCreate extends Activity
|
||||
'modified' => new DateTime(),
|
||||
]);
|
||||
DB::persist($ap_act);
|
||||
$ap_act->setObjectMentionIds($note->_object_mentions_ids);
|
||||
return $ap_act;
|
||||
}
|
||||
}
|
||||
|
@@ -184,14 +184,24 @@ class Note extends Model
|
||||
}
|
||||
}
|
||||
|
||||
$attention_ids = [];
|
||||
$explorer = new Explorer();
|
||||
|
||||
$attention_targets = [];
|
||||
foreach ($to as $target) {
|
||||
if ($target === 'https://www.w3.org/ns/activitystreams#Public') {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$actor = Explorer::getOneFromUri($target);
|
||||
$attention_ids[$actor->getId()] = $target;
|
||||
try {
|
||||
$actor_targets = $explorer->lookup($target);
|
||||
foreach ($actor_targets as $actor) {
|
||||
$attention_targets[$actor->getId()] = $actor;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::debug('ActivityPub->Model->Note->fromJson->Attention->Explorer', [$e]);
|
||||
}
|
||||
$actor = Explorer::getOneFromUri($target);
|
||||
$attention_targets[$actor->getId()] = $actor;
|
||||
// If $to is a group and note is unlisted, set note's scope as Group
|
||||
if ($actor->isGroup() && $map['scope'] === 'unlisted') {
|
||||
$map['scope'] = VisibilityScope::GROUP;
|
||||
@@ -211,29 +221,17 @@ class Note extends Model
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$actor = Explorer::getOneFromUri($target);
|
||||
$attention_ids[$actor->getId()] = $target;
|
||||
} catch (Exception $e) {
|
||||
Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]);
|
||||
}
|
||||
}
|
||||
|
||||
$obj = GSNote::create($map);
|
||||
DB::persist($obj);
|
||||
|
||||
foreach ($attention_ids as $attention_uri) {
|
||||
$explorer = new Explorer();
|
||||
try {
|
||||
$actors = $explorer->lookup($attention_uri);
|
||||
foreach ($actors as $actor) {
|
||||
$object_mention_ids[$target_id = $actor->getId()] = $attention_uri;
|
||||
DB::persist(Attention::create(['note_id' => $obj->getId(), 'target_id' => $target_id]));
|
||||
$actor_targets = $explorer->lookup($target);
|
||||
foreach ($actor_targets as $actor) {
|
||||
$attention_targets[$actor->getId()] = $actor;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::debug('ActivityPub->Model->Note->fromJson->Attention->Explorer', [$e]);
|
||||
}
|
||||
}
|
||||
$attention_ids = array_keys($attention_ids);
|
||||
|
||||
$obj = GSNote::create($map);
|
||||
DB::persist($obj);
|
||||
|
||||
// Attachments
|
||||
$processed_attachments = [];
|
||||
@@ -272,15 +270,15 @@ class Note extends Model
|
||||
}
|
||||
}
|
||||
|
||||
$object_mention_ids = [];
|
||||
$mention_uris = [];
|
||||
foreach ($type_note->get('tag') ?? [] as $ap_tag) {
|
||||
switch ($ap_tag->get('type')) {
|
||||
case 'Mention':
|
||||
$explorer = new Explorer();
|
||||
try {
|
||||
$actors = $explorer->lookup($ap_tag->get('href'));
|
||||
foreach ($actors as $actor) {
|
||||
$object_mention_ids[$actor->getId()] = $ap_tag->get('href');
|
||||
$mention_uris[] = $resource = $ap_tag->get('href');
|
||||
$actor_targets = $explorer->lookup($resource);
|
||||
foreach ($actor_targets as $actor) {
|
||||
$attention_targets[$actor->getId()] = $actor;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::debug('ActivityPub->Model->Note->fromJson->Mention->Explorer', [$e]);
|
||||
@@ -306,14 +304,15 @@ class Note extends Model
|
||||
}
|
||||
|
||||
// The content would be non-sanitized text/html
|
||||
Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = ['TagProcessed' => true, 'ignoreLinks' => $object_mention_ids]]);
|
||||
Event::handle('ProcessNoteContent', [$obj, $obj->getRendered(), $obj->getContentType(), $process_note_content_extra_args = ['TagProcessed' => true, 'ignoreLinks' => $mention_uris]]);
|
||||
|
||||
$object_mention_ids = array_keys($object_mention_ids);
|
||||
$obj->setObjectMentionsIds($object_mention_ids);
|
||||
foreach ($attention_targets as $target) {
|
||||
DB::persist(Attention::create(['object_type' => GSNote::schemaName(), 'object_id' => $obj->getId(), 'target_id' => $target->getId()]));
|
||||
}
|
||||
|
||||
if ($processed_attachments !== []) {
|
||||
foreach ($processed_attachments as [$a, $fname]) {
|
||||
if (DB::count('actor_to_attachment', $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor_id]) === 0) {
|
||||
if (DB::count(ActorToAttachment::class, $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor_id]) === 0) {
|
||||
DB::persist(ActorToAttachment::create($args));
|
||||
}
|
||||
DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $obj->getId(), 'title' => $fname]));
|
||||
@@ -351,11 +350,11 @@ class Note extends Model
|
||||
}
|
||||
|
||||
$attr = [
|
||||
'@context' => ActivityPub::$activity_streams_two_context,
|
||||
'type' => $object->getScope() === VisibilityScope::MESSAGE ? 'ChatMessage' : (match ($object->getType()) {
|
||||
'note' => 'Note',
|
||||
'page' => 'Page',
|
||||
default => throw new \Exception('Unsupported note type.')
|
||||
'@context' => ActivityPub::$activity_streams_two_context,
|
||||
'type' => $object->getScope() === VisibilityScope::MESSAGE ? 'ChatMessage' : (match ($object->getType()) {
|
||||
'note' => 'Note',
|
||||
'page' => 'Page',
|
||||
default => throw new Exception('Unsupported note type.')
|
||||
}),
|
||||
'id' => $object->getUrl(),
|
||||
'published' => $object->getCreated()->format(DateTimeInterface::RFC3339),
|
||||
@@ -370,17 +369,28 @@ class Note extends Model
|
||||
'inConversation' => $object->getConversationUri(),
|
||||
];
|
||||
|
||||
$attentions = $object->getAttentionTargets();
|
||||
// Target scope
|
||||
switch ($object->getScope()) {
|
||||
case VisibilityScope::EVERYWHERE:
|
||||
$attr['to'] = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
$attr['cc'] = [Router::url('actor_subscribers_id', ['id' => $object->getActor()->getId()], Router::ABSOLUTE_URL)];
|
||||
$attr['cc'] = [];
|
||||
foreach ($attentions as $target) {
|
||||
if ($object->getScope() === VisibilityScope::GROUP && $target->isGroup()) {
|
||||
$attr['to'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||
} else {
|
||||
$attr['cc'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case VisibilityScope::LOCAL:
|
||||
throw new ClientException('This note was not federated.', 403);
|
||||
case VisibilityScope::ADDRESSEE:
|
||||
case VisibilityScope::MESSAGE:
|
||||
$attr['to'] = []; // Will be filled later
|
||||
$attr['to'] = [];
|
||||
foreach ($attentions as $target) {
|
||||
$attr['to'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||
}
|
||||
$attr['cc'] = [];
|
||||
break;
|
||||
case VisibilityScope::GROUP:
|
||||
@@ -391,22 +401,19 @@ class Note extends Model
|
||||
// of posts. In this situation, it's safer to always send answers of type unlisted.
|
||||
$attr['to'] = [];
|
||||
$attr['cc'] = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
foreach ($attentions as $target) {
|
||||
if ($object->getScope() === VisibilityScope::GROUP && $target->isGroup()) {
|
||||
$attr['to'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||
} else {
|
||||
$attr['cc'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log::error('ActivityPub->Note->toJson: Found an unknown visibility scope.');
|
||||
throw new ServerException('Found an unknown visibility scope which cannot federate.');
|
||||
}
|
||||
|
||||
// Notification Targets without Mentions
|
||||
$attentions = $object->getNotificationTargets(ids_already_known: ['object' => []]);
|
||||
foreach ($attentions as $target) {
|
||||
if ($object->getScope() === VisibilityScope::GROUP && $target->isGroup()) {
|
||||
$attr['to'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||
} else {
|
||||
$attr['cc'][] = $target->getUri(Router::ABSOLUTE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
// Mentions
|
||||
foreach ($object->getMentionTargets() as $mention) {
|
||||
$attr['tag'][] = [
|
||||
|
Reference in New Issue
Block a user