diff --git a/components/Link/Link.php b/components/Link/Link.php index 305baa35cf..06885173ab 100644 --- a/components/Link/Link.php +++ b/components/Link/Link.php @@ -38,7 +38,7 @@ class Link extends Component /** * Extract URLs from $content and create the appropriate Link and NoteToLink entities */ - public function onProcessNoteContent(Note $note, string $content) + public function onProcessNoteContent(Note $note, string $content): bool { if (Common::config('attachments', 'process_links')) { $matched_urls = []; @@ -56,9 +56,10 @@ class Link extends Component return Event::next; } - public function onRenderContent(string &$text) + public function onRenderPlainTextContent(string &$text): bool { $text = $this->replaceURLs($text); + return Event::next; } public function getURLRegex(): string diff --git a/components/Notification/Entity/Notification.php b/components/Notification/Entity/Notification.php index 2c3afc399a..56e876c2a0 100644 --- a/components/Notification/Entity/Notification.php +++ b/components/Notification/Entity/Notification.php @@ -21,6 +21,7 @@ namespace Component\Notification\Entity; use App\Core\DB\DB; use App\Core\Entity; +use App\Entity\Activity; use App\Entity\Actor; use DateTimeInterface; diff --git a/components/Posting/Posting.php b/components/Posting/Posting.php index cc9522d6f4..b9675f3983 100644 --- a/components/Posting/Posting.php +++ b/components/Posting/Posting.php @@ -155,7 +155,7 @@ class Posting extends Component { $rendered = null; $mentions = []; - Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, &$mentions, $actor, $language]); + Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $language, &$mentions]); $note = Note::create([ 'actor_id' => $actor->getId(), 'content' => $content, @@ -216,7 +216,7 @@ class Posting extends Component return $note; } - public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, array &$mentions, Actor $author, string $language) + public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = []) { switch ($content_type) { case 'text/plain': diff --git a/components/Tag/Tag.php b/components/Tag/Tag.php index 871a94e770..cc4c38db9e 100644 --- a/components/Tag/Tag.php +++ b/components/Tag/Tag.php @@ -60,7 +60,7 @@ class Tag extends Component /** * Process note by extracting any tags present */ - public function onProcessNoteContent(Note $note, string $content) + public function onProcessNoteContent(Note $note, string $content): bool { $matched_tags = []; $processed_tags = false; @@ -75,11 +75,15 @@ class Tag extends Component if ($processed_tags) { DB::flush(); } + return Event::next; } - public function onRenderContent(string &$text, string $language) + public function onRenderPlainTextNoteContent(string &$text, ?string $language = null): bool { - $text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $language), $text); + if (!is_null($language)) { + $text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $language), $text); + } + return Event::next; } private static function tagLink(string $tag, string $language): string @@ -113,7 +117,7 @@ class Tag extends Component * * $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor */ - public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, &$note_expr, &$actor_expr) + public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, &$note_expr, &$actor_expr): bool { $search_term = str_contains($term, ':#') ? explode(':', $term)[1] : $term; $temp_note_expr = $eb->eq('note_tag.tag', $search_term); @@ -132,9 +136,10 @@ class Tag extends Component return Event::stop; } - public function onSeachQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb) + public function onSeachQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool { $note_qb->join('App\Entity\NoteTag', 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id'); $actor_qb->join('App\Entity\ActorTag', 'actor_tag', Expr\Join::WITH, 'actor_tag.tagger = actor.id'); + return Event::next; } } diff --git a/plugins/ActivityPub/ActivityPub.php b/plugins/ActivityPub/ActivityPub.php index 2504dee8c6..d59d45280c 100644 --- a/plugins/ActivityPub/ActivityPub.php +++ b/plugins/ActivityPub/ActivityPub.php @@ -43,7 +43,8 @@ class ActivityPub extends Plugin ]; // So that this isn't hardcoded everywhere - public const PUBLIC_TO = ['https://www.w3.org/ns/activitystreams#Public', + public const PUBLIC_TO = [ + 'https://www.w3.org/ns/activitystreams#Public', 'Public', 'as:Public', ]; @@ -86,6 +87,28 @@ class ActivityPub extends Plugin return Event::next; } + public function onStartGetActorUri(Actor $actor, int $type, ?string &$uri):bool + { + if ( + // Is remote? + !$actor->getIsLocal() + // Is in ActivityPub? + && !is_null($ap_actor = ActivitypubActor::getWithPK(['actor_id' => $actor->getId()])) + // We can only provide a full URL (anything else wouldn't make sense) + && $type === Router::ABSOLUTE_URL + ) { + $uri = $ap_actor->getUri(); + return Event::stop; + } + + return Event::next; + } + + public function onStartGetActorUrl(Actor $actor, int $type, ?string &$url):bool + { + return $this->onStartGetActorUri($actor, $type, $url); + } + public static function getActorByUri(string $resource, ?bool $attempt_fetch = true): Actor { // Try local diff --git a/plugins/ActivityPub/Controller/Inbox.php b/plugins/ActivityPub/Controller/Inbox.php index c38f52104f..8435b41a5a 100644 --- a/plugins/ActivityPub/Controller/Inbox.php +++ b/plugins/ActivityPub/Controller/Inbox.php @@ -60,8 +60,9 @@ class Inbox extends Controller // TODO: Check if Actor has authority over payload // Store Activity - dd(AS2ToEntity::store(activity: $type->toArray(), source: 'ActivityPub')); + $ap_act = AS2ToEntity::store(activity: $type->toArray(), source: 'ActivityPub'); DB::flush(); + dd($ap_act, $act = $ap_act->getActivity(), $act->getActor(), $act->getObject()); return new TypeResponse($type, status: 202); } diff --git a/plugins/ActivityPub/Entity/ActivitypubActivity.php b/plugins/ActivityPub/Entity/ActivitypubActivity.php index 7549162852..81e7c9ca30 100644 --- a/plugins/ActivityPub/Entity/ActivitypubActivity.php +++ b/plugins/ActivityPub/Entity/ActivitypubActivity.php @@ -25,6 +25,7 @@ namespace Plugin\ActivityPub\Entity; use App\Core\DB\DB; use App\Core\Entity; +use App\Entity\Activity; use DateTimeInterface; /** @@ -42,17 +43,24 @@ class ActivitypubActivity extends Entity { // {{{ Autocode // @codeCoverageIgnoreStart + private int $activity_id; private string $activity_uri; - private int $actor_id; - private string $verb; - private string $object_type; - private int $object_id; private string $object_uri; private bool $is_local; - private ?string $source; private DateTimeInterface $created; private DateTimeInterface $modified; + public function setActivityId(int $activity_id): self + { + $this->activity_id = $activity_id; + return $this; + } + + public function getActivityId(): int + { + return $this->activity_id; + } + public function getActivityUri(): string { return $this->activity_uri; @@ -64,50 +72,6 @@ class ActivitypubActivity extends Entity return $this; } - public function setActorId(int $actor_id): self - { - $this->actor_id = $actor_id; - return $this; - } - - public function getActorId(): int - { - return $this->actor_id; - } - - public function setVerb(string $verb): self - { - $this->verb = $verb; - return $this; - } - - public function getVerb(): string - { - return $this->verb; - } - - public function setObjectType(string $object_type): self - { - $this->object_type = $object_type; - return $this; - } - - public function getObjectType(): string - { - return $this->object_type; - } - - public function setObjectId(int $object_id): self - { - $this->object_id = $object_id; - return $this; - } - - public function getObjectId(): int - { - return $this->object_id; - } - public function getObjectUri(): string { return $this->object_uri; @@ -130,17 +94,6 @@ class ActivitypubActivity extends Entity return $this->is_local; } - public function setSource(?string $source): self - { - $this->source = $source; - return $this; - } - - public function getSource(): ?string - { - return $this->source; - } - public function setCreated(DateTimeInterface $created): self { $this->created = $created; @@ -166,19 +119,20 @@ class ActivitypubActivity extends Entity // @codeCoverageIgnoreEnd // }}} Autocode + public function getActivity(): Activity + { + return DB::findOneBy('activity', ['id' => $this->getActivityId()]); + } + public static function schemaDef(): array { return [ 'name' => 'activitypub_activity', 'fields' => [ - 'activity_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Activity\'s URI'], - 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'who made the note'], - 'verb' => ['type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'internal activity verb, influenced by activity pub verbs'], - 'object_type' => ['type' => 'varchar', 'length' => 32, 'description' => 'the name of the table this object refers to'], - 'object_id' => ['type' => 'int', 'description' => 'id in the referenced table'], - 'object_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Object\'s URI'], - 'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'whether this was a locally generated or an imported activity'], - 'source' => ['type' => 'varchar', 'length' => 32, 'description' => 'the source of this activity'], + 'activity_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Activity.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'activity_id to give attention'], + 'activity_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Activity\'s URI'], + 'object_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Object\'s URI'], + 'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'whether this was a locally generated or an imported activity'], 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], ], diff --git a/plugins/ActivityPub/Util/Explorer.php b/plugins/ActivityPub/Util/Explorer.php index a3b28068ef..30288630e7 100644 --- a/plugins/ActivityPub/Util/Explorer.php +++ b/plugins/ActivityPub/Util/Explorer.php @@ -238,7 +238,7 @@ class Explorer 'fullname' => $res['name'] ?? null, 'created' => new DateTime($res['published'] ?? 'now'), 'bio' => isset($res['summary']) ? mb_substr(Security::sanitize($res['summary']), 0, 1000) : null, - 'homepage' => $res['url'] ?? $res['id'], + 'homepage' => $res['url'], 'is_local' => false, 'modified' => new DateTime(), ]; diff --git a/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToEntity.php b/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToEntity.php index 105842dfaf..a40f2691fe 100644 --- a/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToEntity.php +++ b/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToEntity.php @@ -6,6 +6,7 @@ namespace Plugin\ActivityPub\Util\Model\AS2ToEntity; use App\Core\DB\DB; use App\Core\Event; +use App\Entity\Activity; use App\Entity\Actor; use App\Entity\Note; use App\Util\Exception\ClientException; @@ -35,58 +36,47 @@ abstract class AS2ToEntity /** * @throws ClientException */ - public static function store(array $activity, ?string $source = null): array + public static function store(array $activity, ?string $source = null): ActivitypubActivity { - $act = ActivitypubActivity::getWithPK(['activity_uri' => $activity['id']]); - if (\is_null($act)) { + $ap_act = ActivitypubActivity::getWithPK(['activity_uri' => $activity['id']]); + if (\is_null($ap_act)) { $actor = ActivityPub::getActorByUri($activity['actor']); - $map = [ - 'activity_uri' => $activity['id'], + // Store Object + $obj = null; + switch ($activity['object']['type']) { + case 'Note': + $obj = AS2ToNote::translate($activity['object'], $source, $activity['actor'], $actor->getId()); + break; + default: + if (!Event::handle('ActivityPubObject', [$activity['object']['type'], $activity['object'], &$obj])) { + throw new ClientException('Unsupported Object type.'); + } + break; + } + DB::persist($obj); + // Store Activity + $act = Activity::create([ 'actor_id' => $actor->getId(), 'verb' => self::activity_stream_two_verb_to_gs_verb($activity['type']), 'object_type' => self::activity_stream_two_object_type_to_gs_table($activity['object']['type']), + 'object_id' => $obj->getId(), + 'is_local' => false, + 'created' => new DateTime($activity['published'] ?? 'now'), + 'source' => $source, + ]); + DB::persist($act); + // Store ActivityPub Activity + $ap_act = ActivitypubActivity::create([ + 'activity_id' => $act->getId(), + 'activity_uri' => $activity['id'], 'object_uri' => $activity['object']['id'], 'is_local' => false, 'created' => new DateTime($activity['published'] ?? 'now'), 'modified' => new DateTime(), - 'source' => $source, - ]; - - $act = new ActivitypubActivity(); - foreach ($map as $prop => $val) { - $set = Formatting::snakeCaseToCamelCase("set_{$prop}"); - $act->{$set}($val); - } - - $obj = null; - switch ($activity['object']['type']) { - case 'Note': - $obj = AS2ToNote::translate($activity['object'], $source, $activity['actor'], $act); - break; - default: - if (!Event::handle('ActivityPubObject', [$activity['object']['type'], $activity['object'], &$obj])) { - throw new ClientException('Unsupported Object type.'); - } - break; - } - - DB::persist($obj); - $act->setObjectId($obj->getId()); - DB::persist($act); - } else { - $actor = Actor::getById($act->getActorId()); - switch ($activity['object']['type']) { - case 'Note': - $obj = Note::getWithPK(['id' => $act->getObjectId()]); - break; - default: - if (!Event::handle('ActivityPubObject', [$activity['object']['type'], $activity['object'], &$obj])) { - throw new ClientException('Unsupported Object type.'); - } - break; - } + ]); + DB::persist($ap_act); } - return [$actor, $act, $obj]; + return $ap_act; } } diff --git a/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToNote.php b/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToNote.php index dce843e59a..981ad7a42e 100644 --- a/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToNote.php +++ b/plugins/ActivityPub/Util/Model/AS2ToEntity/AS2ToNote.php @@ -6,6 +6,7 @@ namespace Plugin\ActivityPub\Util\Model\AS2ToEntity; use App\Core\Event; use App\Entity\Actor; +use App\Entity\Language; use App\Entity\Note; use App\Util\Formatting; use DateTime; @@ -18,11 +19,9 @@ abstract class AS2ToNote /** *@throws Exception */ - public static function translate(array $object, ?string $source, ?string $actor_uri, ?ActivitypubActivity $act = null): Note + public static function translate(array $object, ?string $source, ?string $actor_uri = null, ?int $actor_id = null): Note { - if (isset($actor_uri) && $actor_uri === $object['attributedTo']) { - $actor_id = $act->getActorId(); - } else { + if (is_null($actor_uri) || $actor_uri !== $object['attributedTo']) { $actor_id = ActivityPub::getActorByUri($object['attributedTo'])->getId(); } $map = [ @@ -30,22 +29,32 @@ abstract class AS2ToNote 'created' => new DateTime($object['published'] ?? 'now'), 'content' => $object['content'] ?? null, 'content_type' => 'text/html', + 'language_id' => $object['contentLang'] ?? null, 'url' => \array_key_exists('url', $object) ? $object['url'] : $object['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::getById($actor_id), - null, // TODO reply to + $map['language_id'], + &$mentions, ]); } $obj = new Note(); + + if (!is_null($map['language_id'])) { + $map['language_id'] = Language::getFromLocale($map['language_id'])->getId(); + } else { + $map['language_id'] = null; + } + foreach ($map as $prop => $val) { $set = Formatting::snakeCaseToCamelCase("set_{$prop}"); $obj->{$set}($val); diff --git a/src/Controller/Actor.php b/src/Controller/Actor.php index 4a2b116b8a..8cd278e8e9 100644 --- a/src/Controller/Actor.php +++ b/src/Controller/Actor.php @@ -25,6 +25,8 @@ namespace App\Controller; use App\Core\Controller; use App\Core\DB\DB; +use App\Core\Router\Router; +use Symfony\Component\HttpFoundation\RedirectResponse; use function App\Core\I18n\_m; use App\Util\Exception\ClientException; use Symfony\Component\HttpFoundation\Request; @@ -37,6 +39,9 @@ class Actor extends Controller private function ActorById(int $id, callable $handle) { $actor = DB::findOneBy('actor', ['id' => $id]); + if ($actor->getIsLocal()) { + return new RedirectResponse(Router::url('actor_view_nickname', ['nickname' => $actor->getNickname()])); + } if (empty($actor)) { throw new ClientException(_m('No such actor.'), 404); } else { diff --git a/src/Entity/Actor.php b/src/Entity/Actor.php index 94922a8505..4ce71eb5ef 100644 --- a/src/Entity/Actor.php +++ b/src/Entity/Actor.php @@ -26,6 +26,7 @@ namespace App\Entity; use App\Core\Cache; use App\Core\DB\DB; use App\Core\Entity; +use App\Core\Event; use App\Core\Router\Router; use App\Core\UserRoles; use App\Util\Common; @@ -432,14 +433,28 @@ class Actor extends Entity ); } - public function getUri(int $type = Router::ABSOLUTE_PATH): string + public function getUri(int $type = Router::ABSOLUTE_URL): string { - return Router::url('actor_view_id', ['id' => $this->getId()], $type); + $uri = null; + if (Event::handle('StartGetActorUri', [$this, $type, &$uri]) === Event::next) { + if ($this->getIsLocal()) { + $uri = Router::url('actor_view_id', ['id' => $this->getId()], $type); + } + Event::handle('EndGetActorUri', [$this, $type, &$uri]); + } + return $uri; } - public function getUrl(int $type = Router::ABSOLUTE_PATH): string + public function getUrl(int $type = Router::ABSOLUTE_URL): string { - return Router::url('actor_view_nickname', ['nickname' => $this->getNickname()], $type); + $url = null; + if (Event::handle('StartGetActorUrl', [$this, $type, &$url]) === Event::next) { + if ($this->getIsLocal()) { + $url = Router::url('actor_view_nickname', ['nickname' => $this->getNickname()], $type); + } + Event::handle('EndGetActorUrl', [$this, $type, &$url]); + } + return $url; } public function getAliases(): array diff --git a/src/Entity/LocalUser.php b/src/Entity/LocalUser.php index 4201abe8c2..79271f87a5 100644 --- a/src/Entity/LocalUser.php +++ b/src/Entity/LocalUser.php @@ -62,7 +62,6 @@ class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUs private ?PhoneNumber $phone_number; private ?int $sms_carrier; private ?string $sms_email; - private ?string $uri; private ?bool $auto_subscribe_back; private ?int $subscription_policy; private ?bool $is_stream_private; @@ -179,17 +178,6 @@ class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUs return $this->sms_email; } - public function setUri(?string $uri): self - { - $this->uri = $uri; - return $this; - } - - public function getUri(): ?string - { - return $this->uri; - } - public function setAutoSubscribeBack(?bool $auto_subscribe_back): self { $this->auto_subscribe_back = $auto_subscribe_back; @@ -399,7 +387,6 @@ class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUs 'phone_number' => ['type' => 'phone_number', 'description' => 'phone number'], 'sms_carrier' => ['type' => 'int', 'foreign key' => true, 'target' => 'SmsCarrier.id', 'multiplicity' => 'one to one', 'description' => 'foreign key to sms_carrier'], 'sms_email' => ['type' => 'varchar', 'length' => 191, 'description' => 'built from sms and carrier (see sms_carrier)'], - 'uri' => ['type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'], 'auto_subscribe_back' => ['type' => 'bool', 'default' => false, 'description' => 'automatically subscribe to users who subscribed us'], 'subscription_policy' => ['type' => 'int', 'size' => 'tiny', 'default' => 0, 'description' => '0 = anybody can subscribe; 1 = require approval'], 'is_stream_private' => ['type' => 'bool', 'default' => false, 'description' => 'whether to limit all notices to subscribers only'], @@ -412,7 +399,6 @@ class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUs 'user_outgoing_email_key' => ['outgoing_email'], 'user_incoming_email_key' => ['incoming_email'], 'user_phone_number_key' => ['phone_number'], - 'user_uri_key' => ['uri'], ], 'indexes' => [ 'user_nickname_idx' => ['nickname'], diff --git a/src/Entity/Note.php b/src/Entity/Note.php index 10574e28e2..66f5723b79 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -55,7 +55,7 @@ class Note extends Entity private ?string $source; private int $scope = VisibilityScope::PUBLIC; private string $url; - private int $language_id; + private ?int $language_id = null; private DateTimeInterface $created; private DateTimeInterface $modified; @@ -166,7 +166,7 @@ class Note extends Entity return $this->language_id; } - public function setLanguageId(int $language_id): self + public function setLanguageId(?int $language_id): self { $this->language_id = $language_id; return $this; diff --git a/src/Util/Formatting.php b/src/Util/Formatting.php index 543e83e537..3067479504 100644 --- a/src/Util/Formatting.php +++ b/src/Util/Formatting.php @@ -237,7 +237,7 @@ abstract class Formatting // Split \n\n into paragraphs, process each paragrah and merge return implode("\n", F\map(explode("\n\n", $text), function (string $paragraph) use ($language) { $paragraph = nl2br($paragraph, use_xhtml: false); - Event::handle('RenderContent', [&$paragraph, $language]); + Event::handle('onRenderPlainTextNoteContent', [&$paragraph, $language]); return HTML::html(['p' => [$paragraph]], options: ['raw' => true, 'indent' => false]); }));