. // }}} /** * ActivityPub implementation for GNU social * * @package GNUsocial * @category ActivityPub * * @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 */ namespace Plugin\ActivityPub\Util\Model; use ActivityPhp\Type; use ActivityPhp\Type\AbstractObject; use App\Core\DB; use App\Core\Event; use App\Core\Log; use App\Core\Router; use App\Entity\Activity as GSActivity; use App\Util\Common; use App\Util\Exception\ClientException; use App\Util\Exception\NoSuchActorException; use App\Util\Exception\NotFoundException; use App\Util\Exception\NotImplementedException; use DateTimeInterface; use Exception; use InvalidArgumentException; use Plugin\ActivityPub\ActivityPub; use Plugin\ActivityPub\Entity\ActivitypubActivity; use Plugin\ActivityPub\Util\Explorer; use Plugin\ActivityPub\Util\Model; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; /** * This class handles translation between JSON and ActivityPub Activities * * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ class Activity extends Model { /** * Create an Entity from an ActivityStreams 2.0 JSON string * This will persist new GSActivities, GSObjects, and APActivity * * @throws ClientExceptionInterface * @throws NoSuchActorException * @throws NotImplementedException * @throws RedirectionExceptionInterface * @throws ServerExceptionInterface * @throws TransportExceptionInterface */ public static function fromJson(string|AbstractObject $json, array $options = []): ActivitypubActivity { $type_activity = \is_string($json) ? self::jsonToType($json) : $json; // Ditch known activities if ($type_activity->has('id')) { // We can't dereference a transient activity $ap_act = DB::findOneBy(ActivitypubActivity::class, ['activity_uri' => $type_activity->get('id')], return_null: true); if (!\is_null($ap_act)) { return $ap_act; } } // Find Actor and Object $actor = Explorer::getOneFromUri($type_activity->get('actor')); $type_object = $type_activity->get('object'); if (\is_string($type_object)) { if (Common::isValidHttpUrl($type_object)) { // Retrieve it $type_object = ActivityPub::getObjectByUri($type_object, try_online: true); } else { $type_object = Type::fromJson($type_object); } } if ($type_object instanceof AbstractObject) { // Encapsulated, if we have it locally, prefer it // TODO: Test authority of activity over object try { $type_object = ActivityPub::getObjectByUri($type_object->get('id'), try_online: false); } catch (Exception $e) { // Use the encapsulated then Log::debug('Failed to find a local activity, will continue with encapsulated.', [$e]); } } if (($type_object instanceof Type\AbstractObject)) { // It's a new object apparently if (Event::handle('NewActivityPubActivity', [$actor, $type_activity, $type_object, &$ap_act]) !== Event::stop) { return self::handle_core_activity($actor, $type_activity, $type_object, $ap_act); } } else { // Object was already stored locally then if (Event::handle('NewActivityPubActivityWithObject', [$actor, $type_activity, $type_object, &$ap_act]) !== Event::stop) { return self::handle_core_activity($actor, $type_activity, $type_object, $ap_act); } } return $ap_act; } private static function handle_core_activity(\App\Entity\Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): ActivitypubActivity { $t = $type_activity->get('type'); switch ($t) { case 'Create': ActivityCreate::handle_core_activity($actor, $type_activity, $type_object, $ap_act); break; case 'Follow': ActivityFollow::handle_core_activity($actor, $type_activity, $type_object, $ap_act); break; 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; switch ($object_type) { case GSActivity::class: switch ($type_object->getVerb()) { case 'subscribe': ActivityFollow::handle_undo($actor, $type_activity, $type_object, $ap_act); break; } break; } break; case 'Announce': ActivityAnnounce::handle_core_activity($actor, $type_activity, $type_object, $ap_act); break; default: Log::debug('ActivityPub: nothing to handle for type ' . $t); } return $ap_act; } /** * Get a JSON * * @throws ClientException */ public static function toType(mixed $object): AbstractObject { if ($object::class !== GSActivity::class) { throw new InvalidArgumentException('First argument type must be an Activity.'); } $gs_verb_to_activity_streams_two_verb = null; if (Event::handle('GSVerbToActivityStreamsTwoActivityType', [($verb = $object->getVerb()), &$gs_verb_to_activity_streams_two_verb]) === Event::next) { $gs_verb_to_activity_streams_two_verb = match ($verb) { 'undo' => 'Undo', 'create' => 'Create', 'subscribe' => 'Follow', default => throw new ClientException('Invalid verb'), }; } $attr = [ 'type' => $gs_verb_to_activity_streams_two_verb, '@context' => ActivityPub::$activity_streams_two_context, 'instrument' => Type::create('Service', [ 'name' => 'GNU social ActivityPub Plugin - v' . ActivityPub::version(), 'url' => GNUSOCIAL_ENGINE_URL, ]), 'id' => Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL), 'published' => $object->getCreated()->format(DateTimeInterface::RFC3339), 'actor' => $object->getActor()->getUri(Router::ABSOLUTE_URL), ]; $attr['to'] = ['https://www.w3.org/ns/activitystreams#Public']; $attr['cc'] = []; foreach ($object->getAttentionTargets() as $target) { $attr['cc'][] = $target->getUri(); } // Get object or Tombstone try { $child = $object->getObject(); // Throws NotFoundException $prefer_embed = ['Create', 'Undo']; $attr['object'] = \in_array($attr['type'], $prefer_embed) ? 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()) { 'note' => Router::url('note_view', ['id' => $object->getObjectId()], type: Router::ABSOLUTE_URL), '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 (!\is_string($attr['object'])) { $attr['@context'] = $attr['object']->get('@context'); $attr['object']->set('@context', null); } $type = self::jsonToType($attr); Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); return $type; } }