. // }}} /** * 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\AbstractObject; use App\Core\DB\DB; use App\Core\Event; use App\Core\GSFile; use App\Core\HTTPClient; use App\Core\Router\Router; use App\Entity\Language; use App\Entity\Note as GSNote; use App\Util\Common; use App\Util\Exception\ClientException; use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\NoSuchActorException; use App\Util\Exception\ServerException; use App\Util\Formatting; use App\Util\TemporaryFile; use Component\Attachment\Entity\ActorToAttachment; use Component\Attachment\Entity\AttachmentToNote; use DateTime; use DateTimeInterface; use Exception; use InvalidArgumentException; use Plugin\ActivityPub\ActivityPub; use Plugin\ActivityPub\Entity\ActivitypubObject; 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; use function App\Core\I18n\_m; use function is_null; use function is_string; /** * This class handles translation between JSON and GSNotes * * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ class Note extends Model { /** * Create an Entity from an ActivityStreams 2.0 JSON string * This will persist a new GSNote * * @param string|AbstractObject $json * @param array $options * @return GSNote * @throws ClientException * @throws DuplicateFoundException * @throws NoSuchActorException * @throws ServerException * @throws ClientExceptionInterface * @throws RedirectionExceptionInterface * @throws ServerExceptionInterface * @throws TransportExceptionInterface */ public static function fromJson(string|AbstractObject $json, array $options = []): GSNote { $source = $options['source'] ?? 'ActivityPub'; $type_note = is_string($json) ? self::jsonToType($json) : $json; $actor = null; $actor_id = null; if ($json instanceof AbstractObject && array_key_exists('test_authority', $options) && $options['test_authority'] && array_key_exists('actor_uri', $options) ) { $actor_uri = $options['actor_uri']; if ($actor_uri !== $type_note->get('attributedTo')) { if (parse_url($actor_uri)['host'] !== parse_url($type_note->get('attributedTo'))['host']) { throw new Exception('You don\'t seem to have enough authority to create this note.'); } } else { $actor = $options['actor'] ?? null; $actor_id = $options['actor_id'] ?? $actor?->getId(); } } if (is_null($actor_id)) { $actor = ActivityPub::getActorByUri($type_note->get('attributedTo')); $actor_id = $actor->getId(); } $map = [ 'is_local' => false, 'created' => new DateTime($type_note->get('published') ?? 'now'), 'content' => $type_note->get('content') ?? null, 'rendered' => null, 'content_type' => 'text/html', 'language_id' => $type_note->get('contentLang') ?? null, 'url' => $type_note->get('url') ?? $type_note->get('id'), 'actor_id' => $actor_id, 'modified' => new DateTime(), 'source' => $source, ]; if ($map['content'] !== null) { $mentions = []; Event::handle('RenderNoteContent', [ $map['content'], $map['content_type'], &$map['rendered'], $actor, $map['language_id'], &$mentions, ]); } $obj = new GSNote(); if (!is_null($map['language_id'])) { $map['language_id'] = Language::getByLocale($map['language_id'])->getId(); } else { $map['language_id'] = null; } foreach ($map as $prop => $val) { $set = Formatting::snakeCaseToCamelCase("set_{$prop}"); $obj->{$set}($val); } // Attachments $processed_attachments = []; foreach ($type_note->get('attachment') as $attachment) { if ($attachment->get('type') === 'Document') { // Retrieve media $get_response = HTTPClient::get($attachment->get('url')); $media = $get_response->getContent(); unset($get_response); // Ignore empty files if (!empty($media)) { // Create an attachment for this $temp_file = new TemporaryFile(); $temp_file->write($media); $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. ' . 'Try to upload a smaller version.', ['quota' => $max_file_size, 'size' => $filesize],)); } Event::handle('EnforceUserFileQuota', [$filesize, $actor_id]); $processed_attachments[] = [GSFile::storeFileAsAttachment($temp_file), $attachment->get('name')]; } } } DB::persist($obj); // Need file and note ids for the next step Event::handle('ProcessNoteContent', [$obj, $obj->getContent(), $obj->getContentType(), $process_note_content_extra_args = []]); 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) { DB::persist(ActorToAttachment::create($args)); } DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $obj->getId(), 'title' => $fname])); } } $map = [ 'object_uri' => $type_note->get('id'), 'object_type' => 'note', 'object_id' => $obj->getId(), 'created' => new DateTime($type_note->get('published') ?? 'now'), 'modified' => new DateTime(), ]; $ap_obj = new ActivitypubObject(); foreach ($map as $prop => $val) { $set = Formatting::snakeCaseToCamelCase("set_{$prop}"); $ap_obj->{$set}($val); } DB::persist($ap_obj); return $obj; } /** * Get a JSON * * @param mixed $object * @param int|null $options * @return string * @throws Exception */ public static function toJson(mixed $object, ?int $options = null): string { if ($object::class !== 'App\Entity\Note') { throw new InvalidArgumentException('First argument type is Note'); } $attr = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => 'Note', 'id' => Router::url('note_view', ['id' => $object->getId()], Router::ABSOLUTE_URL), 'published' => $object->getCreated()->format(DateTimeInterface::RFC3339), 'attributedTo' => $object->getActor()->getUri(Router::ABSOLUTE_URL), 'to' => ['https://www.w3.org/ns/activitystreams#Public'], // TODO: implement proper scope address 'cc' => ['https://www.w3.org/ns/activitystreams#Public'], 'content' => $object->getRendered(), 'attachment' => [], ]; // Attachments foreach ($object->getAttachments() as $attachment) { $attr['attachment'][] = [ 'type' => 'Document', 'mediaType' => $attachment->getMimetype(), 'url' => $attachment->getUrl(Router::ABSOLUTE_URL), 'name' => AttachmentToNote::getByPK(['attachment_id' => $attachment->getId(), 'note_id' => $object->getId()])->getTitle(), 'width' => $attachment->getWidth(), 'height' => $attachment->getHeight(), ]; } $type = self::jsonToType($attr); Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); return $type->toJson($options); } }