diff --git a/components/FreeNetwork/FreeNetwork.php b/components/FreeNetwork/FreeNetwork.php index ea561c0ad3..2733123f26 100644 --- a/components/FreeNetwork/FreeNetwork.php +++ b/components/FreeNetwork/FreeNetwork.php @@ -25,8 +25,6 @@ use App\Core\DB\DB; use App\Core\Event; use App\Core\GSFile; use App\Core\HTTPClient; -use App\Util\Formatting; -use Doctrine\Common\Collections\ExpressionBuilder; use function App\Core\I18n\_m; use App\Core\Log; use App\Core\Modules\Component; @@ -46,6 +44,7 @@ use App\Util\Exception\NicknameTakenException; use App\Util\Exception\NicknameTooLongException; use App\Util\Exception\NoSuchActorException; use App\Util\Exception\ServerException; +use App\Util\Formatting; use App\Util\Nickname; use Component\FreeNetwork\Controller\Feeds; use Component\FreeNetwork\Controller\HostMeta; @@ -55,6 +54,7 @@ use Component\FreeNetwork\Util\Discovery; use Component\FreeNetwork\Util\WebfingerResource; use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor; use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote; +use Doctrine\Common\Collections\ExpressionBuilder; use Exception; use const PREG_SET_ORDER; use Symfony\Component\HttpFoundation\JsonResponse; @@ -78,11 +78,12 @@ class FreeNetwork extends Component public const OAUTH_ACCESS_TOKEN_REL = 'http://apinamespace.org/oauth/access_token'; public const OAUTH_REQUEST_TOKEN_REL = 'http://apinamespace.org/oauth/request_token'; public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize'; - private static array $protocols = []; + private static array $protocols = []; - public function onInitializeComponent() + public function onInitializeComponent(): bool { Event::handle('AddFreeNetworkProtocol', [&self::$protocols]); + return Event::next; } public function onAddRoute(RouteLoader $m): bool diff --git a/plugins/ActivityPub/ActivityPub.php b/plugins/ActivityPub/ActivityPub.php index 1ddc3aa573..382c6d0b5e 100644 --- a/plugins/ActivityPub/ActivityPub.php +++ b/plugins/ActivityPub/ActivityPub.php @@ -53,8 +53,6 @@ use Component\FreeNetwork\Entity\FreeNetworkActorProtocol; use Component\FreeNetwork\Util\Discovery; use Exception; use InvalidArgumentException; -use Plugin\ActivityPub\Util\Response\ActivityResponse; -use Symfony\Component\HttpFoundation\JsonResponse; use const PHP_URL_HOST; use Plugin\ActivityPub\Controller\Inbox; use Plugin\ActivityPub\Controller\Outbox; @@ -64,12 +62,13 @@ use Plugin\ActivityPub\Entity\ActivitypubObject; use Plugin\ActivityPub\Util\HTTPSignature; use Plugin\ActivityPub\Util\Model; use Plugin\ActivityPub\Util\OrderedCollectionController; +use Plugin\ActivityPub\Util\Response\ActivityResponse; use Plugin\ActivityPub\Util\Response\ActorResponse; use Plugin\ActivityPub\Util\Response\NoteResponse; use Plugin\ActivityPub\Util\TypeResponse; use Plugin\ActivityPub\Util\Validator\contentLangModelValidator; use Plugin\ActivityPub\Util\Validator\manuallyApprovesFollowersModelValidator; -use const PREG_SET_ORDER; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; @@ -110,6 +109,27 @@ class ActivityPub extends Plugin return '3.0.0'; } + public static array $activity_streams_two_context = [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ['gs' => 'https://www.gnu.org/software/social/ns#'], + ['litepub' => 'http://litepub.social/ns#'], + ['chatMessage' => 'litepub:chatMessage'], + [ + 'inConversation' => [ + '@id' => 'gs:inConversation', + '@type' => '@id', + ], + ], + ]; + + public function onInitializePlugin(): bool + { + Event::handle('ActivityStreamsTwoContext', [&self::$activity_streams_two_context]); + self::$activity_streams_two_context = array_unique(self::$activity_streams_two_context, \SORT_REGULAR); + return Event::next; + } + /** * This code executes when GNU social creates the page routing, and we hook * on this event to add our Inbox and Outbox handler for ActivityPub. @@ -249,6 +269,7 @@ class ActivityPub extends Plugin * The FreeNetwork component will call this function to pull ActivityPub objects by URI * * @param string $uri Query + * * @return bool true if imported, false otherwise */ public static function freeNetworkGrabRemote(string $uri): bool @@ -258,15 +279,15 @@ class ActivityPub extends Plugin $object = self::getObjectByUri($uri); if (!\is_null($object)) { if ($object instanceof Type\AbstractObject) { - if (in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) { - DB::wrapInTransaction(fn() => Model\Actor::fromJson($object)); + if (\in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) { + DB::wrapInTransaction(fn () => Model\Actor::fromJson($object)); } else { - DB::wrapInTransaction(fn() => Model\Activity::fromJson($object)); + DB::wrapInTransaction(fn () => Model\Activity::fromJson($object)); } } return true; } - } catch (\Exception|\Throwable) { + } catch (Exception|Throwable) { // May be invalid input, we can safely ignore in this case } } @@ -409,7 +430,7 @@ class ActivityPub extends Plugin { try { if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) { - $ap_actor = DB::wrapInTransaction(fn() => ActivitypubActor::getByAddr($addr)); + $ap_actor = DB::wrapInTransaction(fn () => ActivitypubActor::getByAddr($addr)); $actor = Actor::getById($ap_actor->getActorId()); FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor->getId(), $addr); return Event::stop; @@ -467,7 +488,7 @@ class ActivityPub extends Plugin * @throws ServerExceptionInterface * @throws TransportExceptionInterface * - * @return null|mixed|Note|Actor got from URI + * @return null|Actor|mixed|Note got from URI */ public static function getObjectByUri(string $resource, bool $try_online = true) { @@ -485,12 +506,11 @@ class ActivityPub extends Plugin // Try local Note if (Common::isValidHttpUrl($resource)) { - // This means $resource is a valid url $resource_parts = parse_url($resource); // TODO: Use URLMatcher if ($resource_parts['host'] === Common::config('site', 'server')) { $local_note = DB::findOneBy('note', ['url' => $resource], return_null: true); - if ($local_note instanceof Note) { + if (!\is_null($local_note)) { return $local_note; } } @@ -505,14 +525,14 @@ class ActivityPub extends Plugin // Try remote if (!$try_online) { - return null; + return; } $response = HTTPClient::get($resource, ['headers' => self::HTTP_CLIENT_HEADERS]); // If it was deleted if ($response->getStatusCode() == 410) { //$obj = Type::create('Tombstone', ['id' => $resource]); - return null; + return; } elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable throw new Exception('Non Ok Status Code for given Object id.'); } else { diff --git a/plugins/ActivityPub/EVENTS.md b/plugins/ActivityPub/EVENTS.md index bae0fb88ee..c12e0af7bd 100644 --- a/plugins/ActivityPub/EVENTS.md +++ b/plugins/ActivityPub/EVENTS.md @@ -36,10 +36,14 @@ class myValidator extends \Plugin\ActivityPub\Util\ModelValidator ``` **ActivityPubAddActivityStreamsTwoData**: To add attributes to an entity that we are managing to JSON (commonly federating out via ActivityPub) -* `@param string $type_name` When we handle a Type, we will send you the type identifier of the one being handleded +* `@param string $type_name` When we handle a Type, we will send you the type identifier of the one being handled * `@param \ActivityPhp\Type\AbstractObject &$type_activity` The Activity in the intermediate format between Model and JSON * `@return` Returns `Event::next` +**ActivityStreamsTwoContext**: To expand our ActivityStreams 2 Context +* `@param array &$activity_streams_two_context` Append to the array your additional context +* `@return` Returns `Event::next` + **ActivityPubActivityStreamsTwoResponse**: To add a route to ActivityPub (the route must already exist in your plugin) (commonly being requested to ActivityPub) * `@param string $route` Route identifier * `@param array $vars` From your controller diff --git a/plugins/ActivityPub/Util/Model/Activity.php b/plugins/ActivityPub/Util/Model/Activity.php index 3771b3013e..b042fe6137 100644 --- a/plugins/ActivityPub/Util/Model/Activity.php +++ b/plugins/ActivityPub/Util/Model/Activity.php @@ -64,9 +64,6 @@ class Activity extends Model * Create an Entity from an ActivityStreams 2.0 JSON string * This will persist new GSActivities, GSObjects, and APActivity * - * @param string|AbstractObject $json - * @param array $options - * @return ActivitypubActivity * @throws ClientExceptionInterface * @throws NoSuchActorException * @throws NotImplementedException @@ -121,6 +118,7 @@ class Activity extends Model case 'Undo': $object_type = $type_object instanceof AbstractObject ? match ($type_object->get('type')) { 'Note' => \App\Entity\Note::class, + // no break default => throw new NotImplementedException('Unsupported Undo of Object Activity.'), } : $type_object::class; @@ -164,7 +162,7 @@ class Activity extends Model $attr = [ 'type' => $gs_verb_to_activity_streams_two_verb, - '@context' => ['https://www.w3.org/ns/activitystreams'], + '@context' => ActivityPub::$activity_streams_two_context, 'id' => Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL), 'published' => $object->getCreated()->format(DateTimeInterface::RFC3339), 'actor' => $object->getActor()->getUri(Router::ABSOLUTE_URL), diff --git a/plugins/ActivityPub/Util/Model/Actor.php b/plugins/ActivityPub/Util/Model/Actor.php index fe77afbccf..409966c36a 100644 --- a/plugins/ActivityPub/Util/Model/Actor.php +++ b/plugins/ActivityPub/Util/Model/Actor.php @@ -42,7 +42,6 @@ use App\Core\Log; use App\Core\Router\Router; use App\Entity\Actor as GSActor; use App\Util\Exception\ServerException; -use App\Util\Formatting; use App\Util\TemporaryFile; use Component\Avatar\Avatar; use Component\Group\Entity\LocalGroup; @@ -50,6 +49,7 @@ use DateTime; use DateTimeInterface; use Exception; use InvalidArgumentException; +use Plugin\ActivityPub\ActivityPub; use Plugin\ActivityPub\Entity\ActivitypubActor; use Plugin\ActivityPub\Entity\ActivitypubRsa; use Plugin\ActivityPub\Util\Model; @@ -168,12 +168,12 @@ class Actor extends Model $avatar->delete(); } - DB::persist($attachment); - DB::persist(\Component\Avatar\Entity\Avatar::create([ - 'actor_id' => $actor->getId(), - 'attachment_id' => $attachment->getId(), - 'title' => $object->get('icon')->get('name') ?? null, - ])); + DB::persist($attachment); + DB::persist(\Component\Avatar\Entity\Avatar::create([ + 'actor_id' => $actor->getId(), + 'attachment_id' => $attachment->getId(), + 'title' => $object->get('icon')->get('name') ?? null, + ])); Event::handle('AvatarUpdate', [$actor->getId()]); } @@ -212,8 +212,8 @@ class Actor extends Model $public_key = $rsa->getPublicKey(); $uri = $object->getUri(Router::ABSOLUTE_URL); $attr = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'type' => ($object->getType() === GSActor::GROUP) ? (DB::findOneBy(LocalGroup::class, ['actor_id' => $object->getId()], return_null: true)?->getType() === 'organisation' ? 'Organization' : 'Group'): self::$_gs_actor_type_to_as2_actor_type[$object->getType()], + '@context' => ActivityPub::$activity_streams_two_context, + 'type' => ($object->getType() === GSActor::GROUP) ? (DB::findOneBy(LocalGroup::class, ['actor_id' => $object->getId()], return_null: true)?->getType() === 'organisation' ? 'Organization' : 'Group') : self::$_gs_actor_type_to_as2_actor_type[$object->getType()], 'id' => $uri, 'inbox' => Router::url('activitypub_actor_inbox', ['gsactor_id' => $object->getId()], Router::ABSOLUTE_URL), 'outbox' => Router::url('activitypub_actor_outbox', ['gsactor_id' => $object->getId()], Router::ABSOLUTE_URL), diff --git a/plugins/ActivityPub/Util/Model/Note.php b/plugins/ActivityPub/Util/Model/Note.php index 35a3e4e5f0..02627b11bb 100644 --- a/plugins/ActivityPub/Util/Model/Note.php +++ b/plugins/ActivityPub/Util/Model/Note.php @@ -39,13 +39,12 @@ use App\Core\DB\DB; use App\Core\Event; use App\Core\GSFile; use App\Core\HTTPClient; -use App\Entity\NoteType; -use Component\Notification\Entity\Attention; use function App\Core\I18n\_m; use App\Core\Log; use App\Core\Router\Router; use App\Core\VisibilityScope; use App\Entity\Note as GSNote; +use App\Entity\NoteType; use App\Util\Common; use App\Util\Exception\ClientException; use App\Util\Exception\DuplicateFoundException; @@ -59,6 +58,7 @@ use Component\Attachment\Entity\AttachmentToNote; use Component\Conversation\Conversation; use Component\FreeNetwork\FreeNetwork; use Component\Language\Entity\Language; +use Component\Notification\Entity\Attention; use Component\Tag\Entity\NoteTag; use Component\Tag\Tag; use DateTime; @@ -146,7 +146,7 @@ class Note extends Model 'is_local' => false, 'created' => new DateTime($type_note->get('published') ?? 'now'), 'content' => $type_note->get('content') ?? null, - 'rendered' => is_null($type_note->get('content')) ? null : HTML::sanitize($type_note->get('content')), + 'rendered' => \is_null($type_note->get('content')) ? null : HTML::sanitize($type_note->get('content')), 'title' => $type_note->get('name') ?? null, 'content_type' => 'text/html', 'language_id' => $type_note->get('contentLang') ?? null, @@ -154,8 +154,10 @@ class Note extends Model 'actor_id' => $actor_id, 'reply_to' => $reply_to = $handleInReplyTo($type_note), 'modified' => new DateTime(), - 'type' => match ($type_note->get('type')) {'Page' => NoteType::PAGE, default => NoteType::NOTE}, - 'source' => $source, + 'type' => match ($type_note->get('type')) { + 'Page' => NoteType::PAGE, default => NoteType::NOTE + }, + 'source' => $source, ]; if (!\is_null($map['language_id'])) { @@ -188,7 +190,7 @@ class Note extends Model continue; } try { - $actor = ActivityPub::getActorByUri($target); + $actor = ActivityPub::getActorByUri($target); $object_mentions_ids[$actor->getId()] = $target; // If $to is a group and note is unlisted, set note's scope as Group if ($actor->isGroup() && $map['scope'] === 'unlisted') { @@ -209,7 +211,7 @@ class Note extends Model continue; } try { - $actor = ActivityPub::getActorByUri($target); + $actor = ActivityPub::getActorByUri($target); $object_mentions_ids[$actor->getId()] = $target; } catch (Exception $e) { Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); @@ -231,7 +233,7 @@ class Note extends Model // Create an attachment for this $temp_file = new TemporaryFile(); $temp_file->write($media); - $filesize = $temp_file->getSize(); + $filesize = $temp_file->getSize(); $max_file_size = Common::getUploadLimit(); if ($max_file_size < $filesize) { throw new ClientException(_m('No file may be larger than {quota} bytes and the file you sent was {size} bytes. ' @@ -254,7 +256,7 @@ class Note extends Model case 'Mention': case 'Group': try { - $actor = ActivityPub::getActorByUri($ap_tag->get('href')); + $actor = ActivityPub::getActorByUri($ap_tag->get('href')); $object_mentions_ids[$actor->getId()] = $ap_tag->get('href'); } catch (Exception $e) { Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]); @@ -272,8 +274,8 @@ class Note extends Model } break; case 'Hashtag': - $match = ltrim($ap_tag->get('name'), '#'); - $tag = Tag::extract($match); + $match = ltrim($ap_tag->get('name'), '#'); + $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, @@ -334,8 +336,10 @@ class Note extends Model } $attr = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'type' => match($object->getType()) {NoteType::NOTE => 'Note', NoteType::PAGE => 'Page'}, + '@context' => ActivityPub::$activity_streams_two_context, + 'type' => match ($object->getType()) { + NoteType::NOTE => 'Note', NoteType::PAGE => 'Page' + }, 'id' => $object->getUrl(), 'published' => $object->getCreated()->format(DateTimeInterface::RFC3339), 'attributedTo' => $object->getActor()->getUri(Router::ABSOLUTE_URL), @@ -365,7 +369,6 @@ class Note extends Model break; case VisibilityScope::GROUP: // Will have the group in the To coming from attentions - // no break case VisibilityScope::COLLECTION: // Since we don't support sending unlisted/followers-only // notices, arriving here means we're instead answering to that type @@ -379,7 +382,7 @@ class Note extends Model } $attention_cc = DB::findBy(Attention::class, ['note_id' => $object->getId()]); - foreach($attention_cc as $cc_id) { + foreach ($attention_cc as $cc_id) { $target = \App\Entity\Actor::getById($cc_id->getTargetId()); if ($object->getScope() === VisibilityScope::GROUP && $target->isGroup()) { $attr['to'][] = $target->getUri(Router::ABSOLUTE_URL); diff --git a/plugins/ActivityPub/Util/OrderedCollectionController.php b/plugins/ActivityPub/Util/OrderedCollectionController.php index c5df7d6658..a662c66362 100644 --- a/plugins/ActivityPub/Util/OrderedCollectionController.php +++ b/plugins/ActivityPub/Util/OrderedCollectionController.php @@ -38,6 +38,7 @@ use App\Core\Router\Router; use Component\Collection\Util\Controller\CircleController; use Component\Collection\Util\Controller\FeedController; use Component\Collection\Util\Controller\OrderedCollection as GSOrderedCollection; +use Plugin\ActivityPub\ActivityPub; use Symfony\Component\HttpFoundation\Request; /** @@ -80,7 +81,7 @@ abstract class OrderedCollectionController extends GSOrderedCollection { $page = $route_args['page'] ?? 0; $type = $page === 0 ? new OrderedCollection() : new OrderedCollectionPage(); - $type->set('@context', 'https://www.w3.org/ns/activitystreams'); + $type->set('@context', ActivityPub::$activity_streams_two_context); $type->set('items', $ordered_items); $type->set('orderedItems', $ordered_items); $type->set('totalItems', \count($ordered_items)); diff --git a/plugins/PinnedNotes/PinnedNotes.php b/plugins/PinnedNotes/PinnedNotes.php index 90ebc06085..0ea9e2cc71 100644 --- a/plugins/PinnedNotes/PinnedNotes.php +++ b/plugins/PinnedNotes/PinnedNotes.php @@ -145,19 +145,22 @@ class PinnedNotes extends Plugin } // Activity Pub handling stuff + + public function onActivityStreamsTwoContext(array &$activity_streams_two_context): bool + { + $activity_streams_two_context[] = ['toot' => 'http://joinmastodon.org/ns#']; + $activity_streams_two_context[] = [ + 'featured' => [ + '@id' => 'toot:featured', + '@type' => '@id', + ], + ]; + return Event::next; + } + public function onActivityPubAddActivityStreamsTwoData(string $type_name, &$type): bool { - if ($type_name === 'Note') { - $actor = \Plugin\ActivityPub\ActivityPub::getActorByUri($type->get('attributedTo')); - // Note uri is `https://domain:port/object/note/3`. - // is it always like that? I honestly don't know, but I see - // no other way of getting Note id: - $uri_parts = explode('/', $type->get('id')); - $note_id = end($uri_parts); - $is_pinned = !\is_null(DB::findOneBy(E\PinnedNotes::class, ['actor_id' => $actor->getId(), 'note_id' => $note_id], return_null: true)); - - $type->set('featured', $is_pinned); - } elseif ($type_name === 'Person') { + if ($type_name === 'Person') { $actor = \Plugin\ActivityPub\ActivityPub::getActorByUri($type->get('id')); $router_args = ['id' => $actor->getId()]; $router_type = Router::ABSOLUTE_URL; diff --git a/plugins/WebMonetization/WebMonetization.php b/plugins/WebMonetization/WebMonetization.php index 2ffd9d8144..6e03aa5dca 100644 --- a/plugins/WebMonetization/WebMonetization.php +++ b/plugins/WebMonetization/WebMonetization.php @@ -240,6 +240,17 @@ class WebMonetization extends Plugin return Event::next; } + public function onActivityStreamsTwoContext(array &$activity_streams_two_context): bool + { + $activity_streams_two_context[] = [ + 'webmonetizationWallet' => [ + '@id' => 'gs:webmonetizationWallet', + '@type' => '@id', + ], + ]; + return Event::next; + } + public function onActivityPubAddActivityStreamsTwoData(string $type_name, &$type): bool { if ($type_name === 'Person') {