. // }}} /** * ActivityPub implementation for GNU social * * @package GNUsocial * @category ActivityPub * * @author Diogo Peralta Cordeiro <@diogo.site> * @copyright 2018-2019, 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\Entity; use App\Core\Cache; use App\Core\DB\DB; use App\Core\Entity; use function App\Core\I18n\_m; use App\Core\Log; use App\Entity\Actor; use Component\FreeNetwork\Util\Discovery; use DateTimeInterface; use Exception; use Plugin\ActivityPub\Util\DiscoveryHints; use Plugin\ActivityPub\Util\Explorer; use XML_XRD; /** * Table Definition for activitypub_actor * * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ class ActivitypubActor extends Entity { // {{{ Autocode // @codeCoverageIgnoreStart private string $uri; private int $actor_id; private string $inbox_uri; private ?string $inbox_shared_uri = null; private ?string $url = null; private DateTimeInterface $created; private DateTimeInterface $modified; public function setUri(string $uri): self { $this->uri = $uri; return $this; } public function getUri(): string { return $this->uri; } public function setActorId(int $actor_id): self { $this->actor_id = $actor_id; return $this; } public function getActorId(): int { return $this->actor_id; } public function setInboxUri(string $inbox_uri): self { $this->inbox_uri = $inbox_uri; return $this; } public function getInboxUri(): string { return $this->inbox_uri; } public function setInboxSharedUri(?string $inbox_shared_uri): self { $this->inbox_shared_uri = $inbox_shared_uri; return $this; } public function getInboxSharedUri(): ?string { return $this->inbox_shared_uri; } public function setUrl(?string $url): self { $this->url = $url; return $this; } public function getUrl(): ?string { return $this->url; } public function setCreated(DateTimeInterface $created): self { $this->created = $created; return $this; } public function getCreated(): DateTimeInterface { return $this->created; } public function setModified(DateTimeInterface $modified): self { $this->modified = $modified; return $this; } public function getModified(): DateTimeInterface { return $this->modified; } // @codeCoverageIgnoreEnd // }}} Autocode public function getActor(): Actor { return Actor::getById($this->getActorId()); } /** * Look up, and if necessary create, an Activitypub_profile for the remote * entity with the given WebFinger address. * This should never return null -- you will either get an object or * an exception will be thrown. * * @param string $addr WebFinger address * * @throws Exception on error conditions */ public static function getByAddr(string $addr): Actor { // Normalize $addr, i.e. add 'acct:' if missing $addr = Discovery::normalize($addr); // Try the cache $uri = Cache::get(sprintf('ActivitypubActor-webfinger-%s', urlencode($addr)), fn () => false); if ($uri !== false) { if (\is_null($uri)) { // TRANS: Exception. throw new Exception(_m('Not a valid WebFinger address (via cache).')); } try { return DB::wrapInTransaction(fn () => Explorer::getOneFromUri($uri)); } catch (Exception $e) { Log::error(sprintf(__METHOD__ . ': WebFinger address cache inconsistent with database, did not find Activitypub_profile uri==%s', $uri), [$e]); Cache::set(sprintf('ActivitypubActor-webfinger-%s', urlencode($addr)), false); } } // Now, try some discovery $disco = new Discovery(); try { $xrd = $disco->lookup($addr); } catch (Exception $e) { // Save negative cache entry so we don't waste time looking it up again. // @todo FIXME: Distinguish temporary failures? Cache::set(sprintf('ActivitypubActor-webfinger-%s', urlencode($addr)), null); // TRANS: Exception. throw new Exception(_m('Not a valid WebFinger address: ' . $e->getMessage())); } return self::fromXrd($addr, $xrd); } public static function fromXrd(string $addr, XML_XRD $xrd): Actor { $hints = array_merge( ['webfinger' => $addr], DiscoveryHints::fromXRD($xrd), ); if (\array_key_exists('activitypub', $hints)) { $uri = $hints['activitypub']; try { LOG::info("Discovery on acct:{$addr} with URI:{$uri}"); $actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($hints['activitypub'])); Cache::set(sprintf('ActivitypubActor-webfinger-%s', urlencode($addr)), $hints['activitypub']); return $actor; } catch (Exception $e) { Log::warning("Failed creating profile from URI:'{$uri}', error:" . $e->getMessage()); throw $e; // keep looking // // @todo FIXME: This means an error discovering from profile page // may give us a corrupt entry using the webfinger URI, which // will obscure the correct page-keyed profile later on. } } // XXX: try hcard // XXX: try FOAF // TRANS: Exception. %s is a WebFinger address. throw new Exception(sprintf(_m('Could not find a valid profile for "%s".'), $addr)); } /** * @param ActivitypubActor $ap_actor * * @throws Exception */ public static function update_profile(self &$ap_actor, Actor &$actor, ActivitypubRsa &$activitypub_rsa, string $res): void { \Plugin\ActivityPub\Util\Model\Actor::fromJson($res, ['objects' => ['ActivitypubActor' => &$ap_actor, 'Actor' => &$actor, 'ActivitypubRsa' => &$activitypub_rsa]]); } public static function schemaDef(): array { return [ 'name' => 'activitypub_actor', 'fields' => [ 'uri' => ['type' => 'text', 'not null' => true], 'actor_id' => ['type' => 'int', 'not null' => true], 'inbox_uri' => ['type' => 'text', 'not null' => true], 'inbox_shared_uri' => ['type' => 'text'], 'url' => ['type' => 'text'], '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'], ], 'primary key' => ['uri'], 'unique keys' => [ 'activitypub_actor_id_ukey' => ['actor_id'], ], 'foreign keys' => [ 'activitypub_actor_actor_id_fkey' => ['actor', ['actor_id' => 'id']], ], ]; } }