[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 * 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')) { if (Common::config('attachments', 'process_links')) {
$matched_urls = []; $matched_urls = [];
@ -56,9 +56,10 @@ class Link extends Component
return Event::next; return Event::next;
} }
public function onRenderContent(string &$text) public function onRenderPlainTextContent(string &$text): bool
{ {
$text = $this->replaceURLs($text); $text = $this->replaceURLs($text);
return Event::next;
} }
public function getURLRegex(): string public function getURLRegex(): string

View File

@ -21,6 +21,7 @@ namespace Component\Notification\Entity;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use DateTimeInterface; use DateTimeInterface;

View File

@ -155,7 +155,7 @@ class Posting extends Component
{ {
$rendered = null; $rendered = null;
$mentions = []; $mentions = [];
Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, &$mentions, $actor, $language]); Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $language, &$mentions]);
$note = Note::create([ $note = Note::create([
'actor_id' => $actor->getId(), 'actor_id' => $actor->getId(),
'content' => $content, 'content' => $content,
@ -216,7 +216,7 @@ class Posting extends Component
return $note; 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) { switch ($content_type) {
case 'text/plain': case 'text/plain':

View File

@ -60,7 +60,7 @@ class Tag extends Component
/** /**
* Process note by extracting any tags present * Process note by extracting any tags present
*/ */
public function onProcessNoteContent(Note $note, string $content) public function onProcessNoteContent(Note $note, string $content): bool
{ {
$matched_tags = []; $matched_tags = [];
$processed_tags = false; $processed_tags = false;
@ -75,11 +75,15 @@ class Tag extends Component
if ($processed_tags) { if ($processed_tags) {
DB::flush(); 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 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 * $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; $search_term = str_contains($term, ':#') ? explode(':', $term)[1] : $term;
$temp_note_expr = $eb->eq('note_tag.tag', $search_term); $temp_note_expr = $eb->eq('note_tag.tag', $search_term);
@ -132,9 +136,10 @@ class Tag extends Component
return Event::stop; 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'); $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'); $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 // 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', 'Public',
'as:Public', 'as:Public',
]; ];
@ -86,6 +87,28 @@ class ActivityPub extends Plugin
return Event::next; 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 public static function getActorByUri(string $resource, ?bool $attempt_fetch = true): Actor
{ {
// Try local // Try local

View File

@ -60,8 +60,9 @@ class Inbox extends Controller
// TODO: Check if Actor has authority over payload // TODO: Check if Actor has authority over payload
// Store Activity // Store Activity
dd(AS2ToEntity::store(activity: $type->toArray(), source: 'ActivityPub')); $ap_act = AS2ToEntity::store(activity: $type->toArray(), source: 'ActivityPub');
DB::flush(); DB::flush();
dd($ap_act, $act = $ap_act->getActivity(), $act->getActor(), $act->getObject());
return new TypeResponse($type, status: 202); return new TypeResponse($type, status: 202);
} }

View File

@ -25,6 +25,7 @@ namespace Plugin\ActivityPub\Entity;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Activity;
use DateTimeInterface; use DateTimeInterface;
/** /**
@ -42,17 +43,24 @@ class ActivitypubActivity extends Entity
{ {
// {{{ Autocode // {{{ Autocode
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
private int $activity_id;
private string $activity_uri; private string $activity_uri;
private int $actor_id;
private string $verb;
private string $object_type;
private int $object_id;
private string $object_uri; private string $object_uri;
private bool $is_local; private bool $is_local;
private ?string $source;
private DateTimeInterface $created; private DateTimeInterface $created;
private DateTimeInterface $modified; 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 public function getActivityUri(): string
{ {
return $this->activity_uri; return $this->activity_uri;
@ -64,50 +72,6 @@ class ActivitypubActivity extends Entity
return $this; 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 public function getObjectUri(): string
{ {
return $this->object_uri; return $this->object_uri;
@ -130,17 +94,6 @@ class ActivitypubActivity extends Entity
return $this->is_local; 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 public function setCreated(DateTimeInterface $created): self
{ {
$this->created = $created; $this->created = $created;
@ -166,19 +119,20 @@ class ActivitypubActivity extends Entity
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
// }}} Autocode // }}} Autocode
public function getActivity(): Activity
{
return DB::findOneBy('activity', ['id' => $this->getActivityId()]);
}
public static function schemaDef(): array public static function schemaDef(): array
{ {
return [ return [
'name' => 'activitypub_activity', 'name' => 'activitypub_activity',
'fields' => [ 'fields' => [
'activity_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Activity\'s URI'], 'activity_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Activity.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'activity_id to give attention'],
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'who made the note'], 'activity_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Activity\'s URI'],
'verb' => ['type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'internal activity verb, influenced by activity pub verbs'], 'object_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Object\'s URI'],
'object_type' => ['type' => 'varchar', 'length' => 32, 'description' => 'the name of the table this object refers to'], 'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'whether this was a locally generated or an imported activity'],
'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'], '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'], '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, 'fullname' => $res['name'] ?? null,
'created' => new DateTime($res['published'] ?? 'now'), 'created' => new DateTime($res['published'] ?? 'now'),
'bio' => isset($res['summary']) ? mb_substr(Security::sanitize($res['summary']), 0, 1000) : null, 'bio' => isset($res['summary']) ? mb_substr(Security::sanitize($res['summary']), 0, 1000) : null,
'homepage' => $res['url'] ?? $res['id'], 'homepage' => $res['url'],
'is_local' => false, 'is_local' => false,
'modified' => new DateTime(), 'modified' => new DateTime(),
]; ];

View File

@ -6,6 +6,7 @@ namespace Plugin\ActivityPub\Util\Model\AS2ToEntity;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
@ -35,58 +36,47 @@ abstract class AS2ToEntity
/** /**
* @throws ClientException * @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']]); $ap_act = ActivitypubActivity::getWithPK(['activity_uri' => $activity['id']]);
if (\is_null($act)) { if (\is_null($ap_act)) {
$actor = ActivityPub::getActorByUri($activity['actor']); $actor = ActivityPub::getActorByUri($activity['actor']);
$map = [ // Store Object
'activity_uri' => $activity['id'], $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(), 'actor_id' => $actor->getId(),
'verb' => self::activity_stream_two_verb_to_gs_verb($activity['type']), '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_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'], 'object_uri' => $activity['object']['id'],
'is_local' => false, 'is_local' => false,
'created' => new DateTime($activity['published'] ?? 'now'), 'created' => new DateTime($activity['published'] ?? 'now'),
'modified' => new DateTime(), 'modified' => new DateTime(),
'source' => $source, ]);
]; DB::persist($ap_act);
$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;
}
} }
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\Core\Event;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Language;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Formatting; use App\Util\Formatting;
use DateTime; use DateTime;
@ -18,11 +19,9 @@ abstract class AS2ToNote
/** /**
*@throws Exception *@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']) { if (is_null($actor_uri) || $actor_uri !== $object['attributedTo']) {
$actor_id = $act->getActorId();
} else {
$actor_id = ActivityPub::getActorByUri($object['attributedTo'])->getId(); $actor_id = ActivityPub::getActorByUri($object['attributedTo'])->getId();
} }
$map = [ $map = [
@ -30,22 +29,32 @@ abstract class AS2ToNote
'created' => new DateTime($object['published'] ?? 'now'), 'created' => new DateTime($object['published'] ?? 'now'),
'content' => $object['content'] ?? null, 'content' => $object['content'] ?? null,
'content_type' => 'text/html', 'content_type' => 'text/html',
'language_id' => $object['contentLang'] ?? null,
'url' => \array_key_exists('url', $object) ? $object['url'] : $object['id'], 'url' => \array_key_exists('url', $object) ? $object['url'] : $object['id'],
'actor_id' => $actor_id, 'actor_id' => $actor_id,
'modified' => new DateTime(), 'modified' => new DateTime(),
'source' => $source, 'source' => $source,
]; ];
if ($map['content'] !== null) { if ($map['content'] !== null) {
$mentions = [];
Event::handle('RenderNoteContent', [ Event::handle('RenderNoteContent', [
$map['content'], $map['content'],
$map['content_type'], $map['content_type'],
&$map['rendered'], &$map['rendered'],
Actor::getById($actor_id), Actor::getById($actor_id),
null, // TODO reply to $map['language_id'],
&$mentions,
]); ]);
} }
$obj = new Note(); $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) { foreach ($map as $prop => $val) {
$set = Formatting::snakeCaseToCamelCase("set_{$prop}"); $set = Formatting::snakeCaseToCamelCase("set_{$prop}");
$obj->{$set}($val); $obj->{$set}($val);

View File

@ -25,6 +25,8 @@ namespace App\Controller;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Router\Router;
use Symfony\Component\HttpFoundation\RedirectResponse;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -37,6 +39,9 @@ class Actor extends Controller
private function ActorById(int $id, callable $handle) private function ActorById(int $id, callable $handle)
{ {
$actor = DB::findOneBy('actor', ['id' => $id]); $actor = DB::findOneBy('actor', ['id' => $id]);
if ($actor->getIsLocal()) {
return new RedirectResponse(Router::url('actor_view_nickname', ['nickname' => $actor->getNickname()]));
}
if (empty($actor)) { if (empty($actor)) {
throw new ClientException(_m('No such actor.'), 404); throw new ClientException(_m('No such actor.'), 404);
} else { } else {

View File

@ -26,6 +26,7 @@ namespace App\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Event;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Core\UserRoles; use App\Core\UserRoles;
use App\Util\Common; 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 public function getAliases(): array

View File

@ -62,7 +62,6 @@ class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUs
private ?PhoneNumber $phone_number; private ?PhoneNumber $phone_number;
private ?int $sms_carrier; private ?int $sms_carrier;
private ?string $sms_email; private ?string $sms_email;
private ?string $uri;
private ?bool $auto_subscribe_back; private ?bool $auto_subscribe_back;
private ?int $subscription_policy; private ?int $subscription_policy;
private ?bool $is_stream_private; private ?bool $is_stream_private;
@ -179,17 +178,6 @@ class LocalUser extends Entity implements UserInterface, PasswordAuthenticatedUs
return $this->sms_email; 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 public function setAutoSubscribeBack(?bool $auto_subscribe_back): self
{ {
$this->auto_subscribe_back = $auto_subscribe_back; $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'], '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_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)'], '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'], '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'], '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'], '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_outgoing_email_key' => ['outgoing_email'],
'user_incoming_email_key' => ['incoming_email'], 'user_incoming_email_key' => ['incoming_email'],
'user_phone_number_key' => ['phone_number'], 'user_phone_number_key' => ['phone_number'],
'user_uri_key' => ['uri'],
], ],
'indexes' => [ 'indexes' => [
'user_nickname_idx' => ['nickname'], 'user_nickname_idx' => ['nickname'],

View File

@ -55,7 +55,7 @@ class Note extends Entity
private ?string $source; private ?string $source;
private int $scope = VisibilityScope::PUBLIC; private int $scope = VisibilityScope::PUBLIC;
private string $url; private string $url;
private int $language_id; private ?int $language_id = null;
private DateTimeInterface $created; private DateTimeInterface $created;
private DateTimeInterface $modified; private DateTimeInterface $modified;
@ -166,7 +166,7 @@ class Note extends Entity
return $this->language_id; return $this->language_id;
} }
public function setLanguageId(int $language_id): self public function setLanguageId(?int $language_id): self
{ {
$this->language_id = $language_id; $this->language_id = $language_id;
return $this; return $this;

View File

@ -237,7 +237,7 @@ abstract class Formatting
// Split \n\n into paragraphs, process each paragrah and merge // Split \n\n into paragraphs, process each paragrah and merge
return implode("\n", F\map(explode("\n\n", $text), function (string $paragraph) use ($language) { return implode("\n", F\map(explode("\n\n", $text), function (string $paragraph) use ($language) {
$paragraph = nl2br($paragraph, use_xhtml: false); $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]); return HTML::html(['p' => [$paragraph]], options: ['raw' => true, 'indent' => false]);
})); }));