[ActivityPub][Inbox] Restore Create Note Functionality

Minor bug fixes
This commit is contained in:
Diogo Peralta Cordeiro 2021-11-27 15:06:46 +00:00
parent 7145dba8af
commit 56526c9ba6
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
15 changed files with 138 additions and 148 deletions

View File

@ -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

View File

@ -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;

View File

@ -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':

View File

@ -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,12 +75,16 @@ 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
{
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;
}
}

View File

@ -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

View File

@ -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);
}

View File

@ -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_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'],
'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'],
'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'],
],

View File

@ -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(),
];

View File

@ -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);
]);
DB::persist($ap_act);
}
$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;
}
}
return [$actor, $act, $obj];
return $ap_act;
}
}

View File

@ -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);

View File

@ -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 {

View File

@ -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

View File

@ -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'],

View File

@ -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;

View File

@ -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]);
}));