6 Commits
v3 ... v3

354 changed files with 3582 additions and 7051 deletions

View File

@@ -3,4 +3,4 @@ KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st' APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999 SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther PANTHER_APP_ENV=panther
DATABASE_URL=postgresql://postgres:password@db:5432/test DATABASE_URL=postgresql://postgres:password@db:5432/social

View File

@@ -178,7 +178,7 @@ return $config
// There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis. // There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis.
'no_spaces_inside_parenthesis' => true, 'no_spaces_inside_parenthesis' => true,
// Removes `@param`, `@return` and `@var` tags that don't provide any useful information. // Removes `@param`, `@return` and `@var` tags that don't provide any useful information.
'no_superfluous_phpdoc_tags' => false, 'no_superfluous_phpdoc_tags' => true,
// Remove trailing commas in list function calls. // Remove trailing commas in list function calls.
'no_trailing_comma_in_list_call' => true, 'no_trailing_comma_in_list_call' => true,
// PHP single-line arrays should not have trailing comma. // PHP single-line arrays should not have trailing comma.

View File

@@ -22,10 +22,10 @@ declare(strict_types = 1);
namespace Component\Attachment; namespace Component\Attachment;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Formatting; use App\Util\Formatting;
@@ -34,11 +34,10 @@ use Component\Attachment\Entity as E;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
class Attachment extends Component class Attachment extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect('note_attachment_show', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}', [C\Attachment::class, 'attachmentShowWithNote']); $r->connect('note_attachment_show', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}', [C\Attachment::class, 'attachmentShowWithNote']);
$r->connect('note_attachment_view', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}/view', [C\Attachment::class, 'attachmentViewWithNote']); $r->connect('note_attachment_view', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}/view', [C\Attachment::class, 'attachmentViewWithNote']);
@@ -52,13 +51,13 @@ class Attachment extends Component
* *
* This can be used in the future to deduplicate images by visual content * This can be used in the future to deduplicate images by visual content
*/ */
public function onHashFile(string $filename, ?string &$out_hash): EventResult public function onHashFile(string $filename, ?string &$out_hash): bool
{ {
$out_hash = hash_file(E\Attachment::FILEHASH_ALGO, $filename); $out_hash = hash_file(E\Attachment::FILEHASH_ALGO, $filename);
return Event::stop; return Event::stop;
} }
public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult public function onNoteDeleteRelated(Note &$note, Actor $actor): bool
{ {
Cache::delete("note-attachments-{$note->getId()}"); Cache::delete("note-attachments-{$note->getId()}");
foreach ($note->getAttachments() as $attachment) { foreach ($note->getAttachments() as $attachment) {
@@ -69,7 +68,7 @@ class Attachment extends Component
return Event::next; return Event::next;
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
{ {
if (!\in_array('attachment_to_note', $note_qb->getAllAliases())) { if (!\in_array('attachment_to_note', $note_qb->getAllAliases())) {
$note_qb->leftJoin( $note_qb->leftJoin(
@@ -85,7 +84,7 @@ class Attachment extends Component
/** /**
* Populate $note_expr with the criteria for looking for notes with attachments * Populate $note_expr with the criteria for looking for notes with attachments
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool
{ {
$include_term = str_contains($term, ':') ? explode(':', $term)[1] : $term; $include_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;
if (Formatting::startsWith($term, ['note-types:', 'notes-incude:', 'note-filter:'])) { if (Formatting::startsWith($term, ['note-types:', 'notes-incude:', 'note-filter:'])) {

View File

@@ -24,7 +24,7 @@ declare(strict_types = 1);
namespace Component\Attachment\Controller; namespace Component\Attachment\Controller;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Attachment\Entity; namespace Component\Attachment\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use DateTimeInterface; use DateTimeInterface;

View File

@@ -24,13 +24,13 @@ declare(strict_types = 1);
namespace Component\Attachment\Entity; namespace Component\Attachment\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;

View File

@@ -24,12 +24,12 @@ declare(strict_types = 1);
namespace Component\Attachment\Entity; namespace Component\Attachment\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;
use App\Core\Log; use App\Core\Log;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Attachment\Entity; namespace Component\Attachment\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use DateTimeInterface; use DateTimeInterface;

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Attachment\Entity; namespace Component\Attachment\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use DateTimeInterface; use DateTimeInterface;

View File

@@ -23,7 +23,7 @@ declare(strict_types = 1);
namespace Component\Attachment\tests\Controller; namespace Component\Attachment\tests\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Util\GNUsocialTestCase; use App\Util\GNUsocialTestCase;
use Component\Attachment\Entity\Attachment; use Component\Attachment\Entity\Attachment;
use Component\Attachment\Entity\AttachmentToNote; use Component\Attachment\Entity\AttachmentToNote;

View File

@@ -21,10 +21,10 @@ declare(strict_types = 1);
namespace Component\Attachment\tests\Entity; namespace Component\Attachment\tests\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Note; use App\Entity\Note;
use App\Util\GNUsocialTestCase; use App\Util\GNUsocialTestCase;
use App\Util\TemporaryFile; use App\Util\TemporaryFile;
@@ -107,7 +107,7 @@ class AttachmentTest extends GNUsocialTestCase
static::assertSame('Untitled attachment', $attachment->getBestTitle()); static::assertSame('Untitled attachment', $attachment->getBestTitle());
$attachment->setFilename($filename); $attachment->setFilename($filename);
$actor = DB::findOneBy('actor', ['nickname' => 'taken_user']); $actor = DB::findOneBy('actor', ['nickname' => 'taken_user']);
DB::persist($note = Note::create(['actor_id' => $actor->getId(), 'content' => 'attachment: some content', 'content_type' => 'text/plain', 'is_local' => true])); DB::persist($note = Note::create(['actor_id' => $actor->getId(), 'content' => 'attachment: some content', 'content_type' => 'text/plain', 'is_local' => true]));
Conversation::assignLocalConversation($note, null); Conversation::assignLocalConversation($note, null);
DB::persist(AttachmentToNote::create(['attachment_id' => $attachment->getId(), 'note_id' => $note->getId(), 'title' => 'A title'])); DB::persist(AttachmentToNote::create(['attachment_id' => $attachment->getId(), 'note_id' => $note->getId(), 'title' => 'A title']));

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Attachment\tests\Entity; namespace Component\Attachment\tests\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Util\Exception\NotStoredLocallyException; use App\Util\Exception\NotStoredLocallyException;
use App\Util\GNUsocialTestCase; use App\Util\GNUsocialTestCase;

View File

@@ -22,27 +22,26 @@ declare(strict_types = 1);
namespace Component\Avatar; namespace Component\Avatar;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Util\Common; use App\Util\Common;
use Component\Attachment\Entity\Attachment; use Component\Attachment\Entity\Attachment;
use Component\Attachment\Entity\AttachmentThumbnail; use Component\Attachment\Entity\AttachmentThumbnail;
use Component\Avatar\Controller as C; use Component\Avatar\Controller as C;
use Component\Avatar\Exception\NoAvatarException; use Component\Avatar\Exception\NoAvatarException;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Avatar extends Component class Avatar extends Component
{ {
public function onInitializeComponent(): EventResult public function onInitializeComponent()
{ {
return EventResult::next;
} }
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect('avatar_actor', '/actor/{actor_id<\d+>}/avatar/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'avatar_view']); $r->connect('avatar_actor', '/actor/{actor_id<\d+>}/avatar/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'avatar_view']);
$r->connect('avatar_default', '/avatar/default/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'default_avatar_view']); $r->connect('avatar_default', '/avatar/default/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'default_avatar_view']);
@@ -51,11 +50,9 @@ class Avatar extends Component
} }
/** /**
* @param SettingsTabsType $tabs
*
* @throws \App\Util\Exception\ClientException * @throws \App\Util\Exception\ClientException
*/ */
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): EventResult public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): bool
{ {
if ($section === 'profile') { if ($section === 'profile') {
$tabs[] = [ $tabs[] = [
@@ -68,7 +65,7 @@ class Avatar extends Component
return Event::next; return Event::next;
} }
public function onAvatarUpdate(int $actor_id): EventResult public function onAvatarUpdate(int $actor_id): bool
{ {
Cache::delete("avatar-{$actor_id}"); Cache::delete("avatar-{$actor_id}");
foreach (['full', 'big', 'medium', 'small'] as $size) { foreach (['full', 'big', 'medium', 'small'] as $size) {
@@ -131,8 +128,6 @@ class Avatar extends Component
* *
* Returns the avatar file's hash, mimetype, title and path. * Returns the avatar file's hash, mimetype, title and path.
* Ensures exactly one cached value exists * Ensures exactly one cached value exists
*
* @return array{id: null|int, filename: null|string, title: string, mimetype: string, filepath?: string}
*/ */
public static function getAvatarFileInfo(int $actor_id, string $size = 'medium'): array public static function getAvatarFileInfo(int $actor_id, string $size = 'medium'): array
{ {

View File

@@ -24,7 +24,7 @@ declare(strict_types = 1);
namespace Component\Avatar\Controller; namespace Component\Avatar\Controller;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use App\Core\GSFile; use App\Core\GSFile;

View File

@@ -24,10 +24,10 @@ declare(strict_types = 1);
namespace Component\Avatar\Entity; namespace Component\Avatar\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Event; use App\Core\Event;
use App\Core\Router; use App\Core\Router\Router;
use App\Util\Common; use App\Util\Common;
use Component\Attachment\Entity\Attachment; use Component\Attachment\Entity\Attachment;
use Component\Attachment\Entity\AttachmentThumbnail; use Component\Attachment\Entity\AttachmentThumbnail;

View File

@@ -1,7 +1,6 @@
<?php <?php
declare(strict_types = 1); declare(strict_types = 1);
// {{{ License // {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social // This file is part of GNU social - https://www.gnu.org/software/social
@@ -21,14 +20,18 @@ declare(strict_types = 1);
// }}} // }}}
namespace App\Util\Exception; namespace Component\Blog;
use function App\Core\I18n\_m; use App\Core\Event;
use App\Core\Modules\Plugin;
use App\Core\Router\RouteLoader;
use Component\Blog\Controller as C;
class InvalidRequestException extends ClientException class Blog extends Plugin
{ {
public function __construct() public function onAddRoute(RouteLoader $r): bool
{ {
parent::__construct(_m('Invalid request')); $r->connect(id: 'blog_post', uri_path: '/blog/post', target: [C\Post::class, 'makePost']);
return Event::next;
} }
} }

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
// }}} // }}}
namespace Plugin\Blog\Controller; namespace Component\Blog\Controller;
use App\Core\ActorLocalRoles; use App\Core\ActorLocalRoles;
use App\Core\Controller; use App\Core\Controller;
@@ -139,13 +139,14 @@ class Post extends Controller
$extra_args = []; $extra_args = [];
Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form_params, $form]); Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form_params, $form]);
[,$note,] = Posting::storeLocalArticle( [,$note,] = Posting::storeLocalPage(
actor: $actor, actor: $actor,
content: $data['content'], content: $data['content'],
content_type: $content_type, content_type: $content_type,
locale: $data['language'], locale: $data['language'],
scope: VisibilityScope::from($data['visibility']), scope: VisibilityScope::from($data['visibility']),
attentions: [(int) $data['in']], targets: [(int) $data['in']],
reply_to: $data['reply_to_id'],
attachments: $data['attachments'], attachments: $data['attachments'],
process_note_content_extra_args: $extra_args, process_note_content_extra_args: $extra_args,
title: $data['title'], title: $data['title'],

View File

@@ -24,14 +24,16 @@ declare(strict_types = 1);
namespace Component\Circle; namespace Component\Circle;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Feed; use App\Entity\Feed;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Util\Common;
use App\Util\Nickname; use App\Util\Nickname;
use Component\Circle\Controller as CircleController; use Component\Circle\Controller as CircleController;
use Component\Circle\Entity\ActorCircle; use Component\Circle\Entity\ActorCircle;
@@ -39,7 +41,6 @@ use Component\Circle\Entity\ActorCircleSubscription;
use Component\Circle\Entity\ActorTag; use Component\Circle\Entity\ActorTag;
use Component\Collection\Util\MetaCollectionTrait; use Component\Collection\Util\MetaCollectionTrait;
use Component\Tag\Tag; use Component\Tag\Tag;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -54,13 +55,12 @@ use Symfony\Component\HttpFoundation\Request;
*/ */
class Circle extends Component class Circle extends Component
{ {
/** @phpstan-use MetaCollectionTrait<ActorCircle> */
use MetaCollectionTrait; use MetaCollectionTrait;
public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/'; public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/';
protected const SLUG = 'circle'; protected const SLUG = 'circle';
protected const PLURAL_SLUG = 'circles'; protected const PLURAL_SLUG = 'circles';
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect('actor_circle_view_by_circle_id', '/circle/{circle_id<\d+>}', [CircleController\Circle::class, 'circleById']); $r->connect('actor_circle_view_by_circle_id', '/circle/{circle_id<\d+>}', [CircleController\Circle::class, 'circleById']);
// View circle members by (tagger id or nickname) and tag // View circle members by (tagger id or nickname) and tag
@@ -95,23 +95,20 @@ class Circle extends Component
]; ];
} }
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): EventResult public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
{ {
if ($section === 'profile' && \in_array($request->get('_route'), ['person_actor_settings', 'group_actor_settings'])) { if ($section === 'profile' && \in_array($request->get('_route'), ['person_actor_settings', 'group_actor_settings'])) {
$tabs[] = [ $tabs[] = [
'title' => _m('Self tags'), 'title' => 'Self tags',
'desc' => _m('Add or remove tags to this actor'), 'desc' => 'Add or remove tags on yourself',
'id' => 'settings-self-tags', 'id' => 'settings-self-tags',
'controller' => CircleController\SelfTagsSettings::settingsSelfTags($request, Actor::getById((int) $request->get('id')), 'settings-self-tags-details'), 'controller' => CircleController\SelfTagsSettings::settingsSelfTags($request, Common::actor(), 'settings-self-tags-details'),
]; ];
} }
return Event::next; return Event::next;
} }
/** public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): bool
* @param Actor[] $targets
*/
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): EventResult
{ {
$circles = $actor->getCircles(); $circles = $actor->getCircles();
foreach ($circles as $circle) { foreach ($circles as $circle) {
@@ -123,9 +120,6 @@ class Circle extends Component
// Meta Collection ------------------------------------------------------------------- // Meta Collection -------------------------------------------------------------------
/**
* @param array<string, mixed> $vars
*/
private function getActorIdFromVars(array $vars): int private function getActorIdFromVars(array $vars): int
{ {
$id = $vars['request']->get('id', null); $id = $vars['request']->get('id', null);
@@ -137,7 +131,7 @@ class Circle extends Component
return $user->getId(); return $user->getId();
} }
public static function createCircle(Actor|int $tagger_id, string $tag): int|null public static function createCircle(Actor|int $tagger_id, string $tag): int
{ {
$tagger_id = \is_int($tagger_id) ? $tagger_id : $tagger_id->getId(); $tagger_id = \is_int($tagger_id) ? $tagger_id : $tagger_id->getId();
$circle = ActorCircle::create([ $circle = ActorCircle::create([
@@ -153,10 +147,7 @@ class Circle extends Component
return $circle->getId(); return $circle->getId();
} }
/** protected function createCollection(Actor $owner, array $vars, string $name)
* @param array<string, mixed> $vars
*/
protected function createCollection(Actor $owner, array $vars, string $name): void
{ {
$this->createCircle($owner, $name); $this->createCircle($owner, $name);
DB::persist(ActorTag::create([ DB::persist(ActorTag::create([
@@ -166,12 +157,7 @@ class Circle extends Component
])); ]));
} }
/** protected function removeItem(Actor $owner, array $vars, $items, array $collections)
* @param array<string, mixed> $vars
* @param array<int> $items
* @param array<mixed> $collections
*/
protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool
{ {
$tagger_id = $owner->getId(); $tagger_id = $owner->getId();
$tagged_id = $this->getActorIdFromVars($vars); $tagged_id = $this->getActorIdFromVars($vars);
@@ -184,15 +170,9 @@ class Circle extends Component
DB::removeBy(ActorTag::class, ['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]); DB::removeBy(ActorTag::class, ['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]);
} }
Cache::delete(Actor::cacheKeys($tagger_id)['circles']); Cache::delete(Actor::cacheKeys($tagger_id)['circles']);
return true;
} }
/** protected function addItem(Actor $owner, array $vars, $items, array $collections)
* @param array<string, mixed> $vars
* @param array<int> $items
* @param array<mixed> $collections
*/
protected function addItem(Actor $owner, array $vars, array $items, array $collections): void
{ {
$tagger_id = $owner->getId(); $tagger_id = $owner->getId();
$tagged_id = $this->getActorIdFromVars($vars); $tagged_id = $this->getActorIdFromVars($vars);
@@ -209,10 +189,8 @@ class Circle extends Component
/** /**
* @see MetaCollectionPlugin->shouldAddToRightPanel * @see MetaCollectionPlugin->shouldAddToRightPanel
*
* @param array<string, mixed> $vars
*/ */
protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool
{ {
return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id']); return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id']);
} }
@@ -224,11 +202,9 @@ class Circle extends Component
* Differs from the overwritten method in MetaCollectionsTrait, since retrieved Collections come from the $owner * Differs from the overwritten method in MetaCollectionsTrait, since retrieved Collections come from the $owner
* itself, and from every Actor that is a part of its ActorCircle. * itself, and from every Actor that is a part of its ActorCircle.
* *
* @param Actor $owner the Actor, and by extension its own circle of Actors * @param Actor $owner the Actor, and by extension its own circle of Actors
* @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event * @param null|array $vars Page vars sent by AppendRightPanelBlock event
* @param bool $ids_only true if only the Collections ids are to be returned * @param bool $ids_only true if only the Collections ids are to be returned
*
* @return ($ids_only is true ? int[] : ActorCircle[])
*/ */
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
{ {
@@ -244,7 +220,7 @@ class Circle extends Component
return $ids_only ? array_map(fn ($x) => $x->getId(), $circles) : $circles; return $ids_only ? array_map(fn ($x) => $x->getId(), $circles) : $circles;
} }
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering)
{ {
DB::persist(Feed::create([ DB::persist(Feed::create([
'actor_id' => $actor_id, 'actor_id' => $actor_id,

View File

@@ -38,8 +38,6 @@ class Circle extends CircleController
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
*
* @return ControllerResultType
*/ */
public function circleById(int|ActorCircle $circle_id): array public function circleById(int|ActorCircle $circle_id): array
{ {
@@ -59,17 +57,11 @@ class Circle extends CircleController
} }
} }
/**
* @return ControllerResultType
*/
public function circleByTaggerIdAndTag(int $tagger_id, string $tag): array public function circleByTaggerIdAndTag(int $tagger_id, string $tag): array
{ {
return $this->circleById(ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag])); return $this->circleById(ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag]));
} }
/**
* @return ControllerResultType
*/
public function circleByTaggerNicknameAndTag(string $tagger_nickname, string $tag): array public function circleByTaggerNicknameAndTag(string $tagger_nickname, string $tag): array
{ {
return $this->circleById(ActorCircle::getByPK(['tagger' => LocalUser::getByNickname($tagger_nickname)->getId(), 'tag' => $tag])); return $this->circleById(ActorCircle::getByPK(['tagger' => LocalUser::getByNickname($tagger_nickname)->getId(), 'tag' => $tag]));

View File

@@ -24,27 +24,23 @@ declare(strict_types = 1);
namespace Component\Circle\Controller; namespace Component\Circle\Controller;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use Component\Circle\Entity\ActorCircle; use Component\Circle\Entity\ActorCircle;
use Component\Collection\Util\Controller\MetaCollectionController; use Component\Collection\Util\Controller\MetaCollectionController;
/**
* @extends MetaCollectionController<Circles>
*/
class Circles extends MetaCollectionController class Circles extends MetaCollectionController
{ {
protected const SLUG = 'circle'; protected const SLUG = 'circle';
protected const PLURAL_SLUG = 'circles'; protected const PLURAL_SLUG = 'circles';
protected string $page_title = 'Actor circles'; protected string $page_title = 'Actor circles';
public function createCollection(int $owner_id, string $name): bool public function createCollection(int $owner_id, string $name)
{ {
return !\is_null(\Component\Circle\Circle::createCircle($owner_id, $name)); return \Component\Circle\Circle::createCircle($owner_id, $name);
} }
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
{ {
return Router::url( return Router::url(
@@ -53,26 +49,21 @@ class Circles extends MetaCollectionController
); );
} }
/** public function getCollectionItems(int $owner_id, $collection_id): array
* @return Circles[]
*/
public function getCollectionItems(int $owner_id, int $collection_id): array
{ {
return []; // TODO $notes = []; // TODO: Use Feed::query
return [
'_template' => 'collection/notes.html.twig',
'notes' => $notes,
];
} }
/**
* @return Circles[]
*/
public function feedByCircleId(int $circle_id) public function feedByCircleId(int $circle_id)
{ {
// Owner id isn't used // Owner id isn't used
return $this->getCollectionItems(0, $circle_id); return $this->getCollectionItems(0, $circle_id);
} }
/**
* @return Circles[]
*/
public function feedByTaggerIdAndTag(int $tagger_id, string $tag) public function feedByTaggerIdAndTag(int $tagger_id, string $tag)
{ {
// Owner id isn't used // Owner id isn't used
@@ -80,9 +71,6 @@ class Circles extends MetaCollectionController
return $this->getCollectionItems($tagger_id, $circle_id); return $this->getCollectionItems($tagger_id, $circle_id);
} }
/**
* @return Circles[]
*/
public function feedByTaggerNicknameAndTag(string $tagger_nickname, string $tag) public function feedByTaggerNicknameAndTag(string $tagger_nickname, string $tag)
{ {
$tagger_id = LocalUser::getByNickname($tagger_nickname)->getId(); $tagger_id = LocalUser::getByNickname($tagger_nickname)->getId();
@@ -94,13 +82,12 @@ class Circles extends MetaCollectionController
{ {
return DB::findBy(ActorCircle::class, ['tagger' => $owner_id], order_by: ['id' => 'desc']); return DB::findBy(ActorCircle::class, ['tagger' => $owner_id], order_by: ['id' => 'desc']);
} }
public function getCollectionBy(int $owner_id, int $collection_id): ActorCircle
public function getCollectionBy(int $owner_id, int $collection_id): self
{ {
return DB::findOneBy(ActorCircle::class, ['id' => $collection_id, 'actor_id' => $owner_id]); return DB::findOneBy(ActorCircle::class, ['id' => $collection_id, 'actor_id' => $owner_id]);
} }
public function setCollectionName(int $actor_id, string $actor_nickname, ActorCircle $collection, string $name): void public function setCollectionName(int $actor_id, string $actor_nickname, ActorCircle $collection, string $name)
{ {
foreach ($collection->getActorTags(db_reference: true) as $at) { foreach ($collection->getActorTags(db_reference: true) as $at) {
$at->setTag($name); $at->setTag($name);
@@ -109,7 +96,7 @@ class Circles extends MetaCollectionController
Cache::delete(Actor::cacheKeys($actor_id)['circles']); Cache::delete(Actor::cacheKeys($actor_id)['circles']);
} }
public function removeCollection(int $actor_id, string $actor_nickname, ActorCircle $collection): void public function removeCollection(int $actor_id, string $actor_nickname, ActorCircle $collection)
{ {
foreach ($collection->getActorTags(db_reference: true) as $at) { foreach ($collection->getActorTags(db_reference: true) as $at) {
DB::remove($at); DB::remove($at);

View File

@@ -6,7 +6,7 @@ namespace Component\Circle\Controller;
use App\Core\Cache; use App\Core\Cache;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Entity as E; use App\Entity as E;
use App\Util\Common; use App\Util\Common;
@@ -45,7 +45,7 @@ class SelfTagsSettings extends Controller
foreach ($tags as $tag) { foreach ($tags as $tag) {
$tag = CompTag::sanitize($tag); $tag = CompTag::sanitize($tag);
[$actor_tag, $actor_tag_existed] = ActorTag::checkExistingAndCreateOrUpdate([ [$actor_tag, $actor_tag_existed] = ActorTag::createOrUpdate([
'tagger' => $target->getId(), // self tag means tagger = tagger in ActorTag 'tagger' => $target->getId(), // self tag means tagger = tagger in ActorTag
'tagged' => $target->getId(), 'tagged' => $target->getId(),
'tag' => $tag, 'tag' => $tag,

View File

@@ -22,10 +22,9 @@ declare(strict_types = 1);
namespace Component\Circle\Entity; namespace Component\Circle\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor;
use DateTimeInterface; use DateTimeInterface;
/** /**
@@ -145,9 +144,6 @@ class ActorCircle extends Entity
return $this->tag; return $this->tag;
} }
/**
* @return ActorTag[]
*/
public function getActorTags(bool $db_reference = false): array public function getActorTags(bool $db_reference = false): array
{ {
$handle = fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'tag' => $this->getTag()]); $handle = fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'tag' => $this->getTag()]);
@@ -160,10 +156,7 @@ class ActorCircle extends Entity
); );
} }
/** public function getTaggedActors()
* @return Actor[]
*/
public function getTaggedActors(): array
{ {
return Cache::get( return Cache::get(
"circle-{$this->getId()}-tagged-actors", "circle-{$this->getId()}-tagged-actors",
@@ -177,9 +170,6 @@ class ActorCircle extends Entity
); );
} }
/**
* @return Actor[]
*/
public function getSubscribedActors(?int $offset = null, ?int $limit = null): array public function getSubscribedActors(?int $offset = null, ?int $limit = null): array
{ {
return Cache::get( return Cache::get(

View File

@@ -21,9 +21,9 @@ declare(strict_types = 1);
namespace Component\Circle\Entity; namespace Component\Circle\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use Component\Tag\Tag; use Component\Tag\Tag;
use DateTimeInterface; use DateTimeInterface;

View File

@@ -7,18 +7,14 @@ namespace Component\Circle\Form;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Util\Form\ArrayTransformer; use App\Util\Form\ArrayTransformer;
use Component\Circle\Entity\ActorTag;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
abstract class SelfTagsForm abstract class SelfTagsForm
{ {
/** /**
* @param ActorTag[] $actor_self_tags * @return array [Form (add), ?Form (existing)]
*
* @return array{FormInterface, ?FormInterface} [Form (add), ?Form (existing)]
*/ */
public static function handleTags( public static function handleTags(
Request $request, Request $request,
@@ -38,7 +34,7 @@ abstract class SelfTagsForm
$existing_form = !empty($form_definition) ? Form::create($form_definition) : null; $existing_form = !empty($form_definition) ? Form::create($form_definition) : null;
$add_form = Form::create([ $add_form = Form::create([
['new-tags', TextType::class, ['label' => ' ', 'data' => [], 'required' => false, 'help' => _m('Tags for this actor (letters, numbers, -, ., and _), comma- or space-separated.'), 'transformer' => ArrayTransformer::class]], ['new-tags', TextType::class, ['label' => ' ', 'data' => [], 'required' => false, 'help' => _m('Tags for yourself (letters, numbers, -, ., and _), comma- or space-separated.'), 'transformer' => ArrayTransformer::class]],
[$add_form_name = 'new-tags-add', SubmitType::class, ['label' => $add_label]], [$add_form_name = 'new-tags-add', SubmitType::class, ['label' => $add_label]],
]); ]);

View File

@@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace Component\Collection; namespace Component\Collection;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Entity\Actor; use App\Entity\Actor;
@@ -14,7 +14,6 @@ use Component\Subscription\Entity\ActorSubscription;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
class Collection extends Component class Collection extends Component
{ {
@@ -23,11 +22,6 @@ class Collection extends Component
* *
* Supports a variety of query terms and is used both in feeds and * Supports a variety of query terms and is used both in feeds and
* in search. Uses query builders to allow for extension * in search. Uses query builders to allow for extension
*
* @param array<string, OrderByType> $note_order_by
* @param array<string, OrderByType> $actor_order_by
*
* @return array{notes: null|Note[], actors: null|Actor[]}
*/ */
public static function query(string $query, int $page, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array public static function query(string $query, int $page, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
{ {
@@ -70,7 +64,7 @@ class Collection extends Component
return ['notes' => $notes ?? null, 'actors' => $actors ?? null]; return ['notes' => $notes ?? null, 'actors' => $actors ?? null];
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
{ {
$note_aliases = $note_qb->getAllAliases(); $note_aliases = $note_qb->getAllAliases();
if (!\in_array('subscription', $note_aliases)) { if (!\in_array('subscription', $note_aliases)) {
@@ -85,60 +79,57 @@ class Collection extends Component
/** /**
* Convert $term to $note_expr and $actor_expr, search criteria. Handles searching for text * Convert $term to $note_expr and $actor_expr, search criteria. Handles searching for text
* notes, for different types of actors and for the content of text notes * notes, for different types of actors and for the content of text notes
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr)
{ {
if (str_contains($term, ':')) { if (str_contains($term, ':')) {
$term = explode(':', $term); $term = explode(':', $term);
if (Formatting::startsWith($term[0], 'note')) { if (Formatting::startsWith($term[0], 'note')) {
switch ($term[0]) { switch ($term[0]) {
case 'notes-all': case 'notes-all':
$note_expr = $eb->neq('note.created', null); $note_expr = $eb->neq('note.created', null);
break; break;
case 'note-local': case 'note-local':
$note_expr = $eb->eq('note.is_local', filter_var($term[1], \FILTER_VALIDATE_BOOLEAN)); $note_expr = $eb->eq('note.is_local', filter_var($term[1], \FILTER_VALIDATE_BOOLEAN));
break; break;
case 'note-types': case 'note-types':
case 'notes-include': case 'notes-include':
case 'note-filter': case 'note-filter':
if (\is_null($note_expr)) { if (\is_null($note_expr)) {
$note_expr = []; $note_expr = [];
} }
if (array_intersect(explode(',', $term[1]), ['text', 'words']) !== []) { if (array_intersect(explode(',', $term[1]), ['text', 'words']) !== []) {
$note_expr[] = $eb->neq('note.content', null); $note_expr[] = $eb->neq('note.content', null);
} else { } else {
$note_expr[] = $eb->eq('note.content', null); $note_expr[] = $eb->eq('note.content', null);
} }
break; break;
case 'note-conversation': case 'note-conversation':
$note_expr = $eb->eq('note.conversation_id', (int) trim($term[1])); $note_expr = $eb->eq('note.conversation_id', (int) trim($term[1]));
break; break;
case 'note-from': case 'note-from':
case 'notes-from': case 'notes-from':
$subscribed_expr = $eb->eq('subscription.subscriber_id', $actor->getId()); $subscribed_expr = $eb->eq('subscription.subscriber_id', $actor->getId());
$type_consts = []; $type_consts = [];
if ($term[1] === 'subscribed') { if ($term[1] === 'subscribed') {
$type_consts = null; $type_consts = null;
} }
foreach (explode(',', $term[1]) as $from) { foreach (explode(',', $term[1]) as $from) {
if (str_starts_with($from, 'subscribed-')) { if (str_starts_with($from, 'subscribed-')) {
[, $type] = explode('-', $from); [, $type] = explode('-', $from);
if (\in_array($type, ['actor', 'actors'])) { if (\in_array($type, ['actor', 'actors'])) {
$type_consts = null; $type_consts = null;
} else { } else {
$type_consts[] = \constant(Actor::class . '::' . mb_strtoupper($type === 'organisation' ? 'group' : $type)); $type_consts[] = \constant(Actor::class . '::' . mb_strtoupper($type === 'organisation' ? 'group' : $type));
}
} }
} }
if (\is_null($type_consts)) { }
$note_expr = $subscribed_expr; if (\is_null($type_consts)) {
} elseif (!empty($type_consts)) { $note_expr = $subscribed_expr;
$note_expr = $eb->andX($subscribed_expr, $eb->in('note_actor.type', $type_consts)); } elseif (!empty($type_consts)) {
} $note_expr = $eb->andX($subscribed_expr, $eb->in('note_actor.type', $type_consts));
break; }
break;
} }
} elseif (Formatting::startsWith($term, 'actor-')) { } elseif (Formatting::startsWith($term, 'actor-')) {
switch ($term[0]) { switch ($term[0]) {
@@ -152,8 +143,8 @@ class Collection extends Component
foreach ( foreach (
[ [
Actor::PERSON => ['person', 'people'], Actor::PERSON => ['person', 'people'],
Actor::GROUP => ['group', 'groups', 'org', 'orgs', 'organisation', 'organisations', 'organization', 'organizations'], Actor::GROUP => ['group', 'groups', 'org', 'orgs', 'organisation', 'organisations', 'organization', 'organizations'],
Actor::BOT => ['bot', 'bots'], Actor::BOT => ['bot', 'bots'],
] as $type => $match) { ] as $type => $match) {
if (array_intersect(explode(',', $term[1]), $match) !== []) { if (array_intersect(explode(',', $term[1]), $match) !== []) {
$actor_expr[] = $eb->eq('actor.type', $type); $actor_expr[] = $eb->eq('actor.type', $type);

View File

@@ -4,9 +4,6 @@ declare(strict_types = 1);
namespace Component\Collection\Util\Controller; namespace Component\Collection\Util\Controller;
/**
* @extends OrderedCollection<\Component\Circle\Entity\ActorCircle>
*/
class CircleController extends OrderedCollection class CircleController extends OrderedCollection
{ {
} }

View File

@@ -6,25 +6,15 @@ namespace Component\Collection\Util\Controller;
use App\Core\Controller; use App\Core\Controller;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use Component\Collection\Collection as CollectionComponent; use Component\Collection\Collection as CollectionModule;
/** class Collection extends Controller
* @template T
*/
abstract class Collection extends Controller
{ {
/**
* @param array<string, OrderByType> $note_order_by
* @param array<string, OrderByType> $actor_order_by
*
* @return array{notes: null|Note[], actors: null|Actor[]}
*/
public function query(string $query, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array public function query(string $query, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
{ {
$actor ??= Common::actor(); $actor ??= Common::actor();
$locale ??= Common::currentLanguage()->getLocale(); $locale ??= Common::currentLanguage()->getLocale();
return CollectionComponent::query($query, $this->int('page') ?? 1, $locale, $actor, $note_order_by, $actor_order_by); return CollectionModule::query($query, $this->int('page') ?? 1, $locale, $actor, $note_order_by, $actor_order_by);
} }
} }

View File

@@ -38,23 +38,12 @@ use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use Functional as F; use Functional as F;
/**
* @template T
*
* @extends OrderedCollection<T>
*/
abstract class FeedController extends OrderedCollection abstract class FeedController extends OrderedCollection
{ {
/** /**
* Post-processing of the result of a feed controller, to remove any * Post-processing of the result of a feed controller, to remove any
* notes or actors the user specified, as well as format the raw * notes or actors the user specified, as well as format the raw
* list of notes into a usable format * list of notes into a usable format
*
* @template NA of Note|Actor
*
* @param NA[] $result
*
* @return NA[]
*/ */
protected function postProcess(array $result): array protected function postProcess(array $result): array
{ {
@@ -69,9 +58,6 @@ abstract class FeedController extends OrderedCollection
return $result; return $result;
} }
/**
* @param Note[] $notes
*/
private static function enforceScope(array &$notes, ?Actor $actor, ?Actor $in = null): void private static function enforceScope(array &$notes, ?Actor $actor, ?Actor $in = null): void
{ {
$notes = F\select($notes, fn (Note $n) => $n->isVisibleTo($actor, $in)); $notes = F\select($notes, fn (Note $n) => $n->isVisibleTo($actor, $in));

View File

@@ -31,7 +31,7 @@ declare(strict_types = 1);
namespace Component\Collection\Util\Controller; namespace Component\Collection\Util\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Entity\LocalUser; use App\Entity\LocalUser;
@@ -39,14 +39,8 @@ use App\Util\Common;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @template T of object
*
* @extends FeedController<T>
*/
abstract class MetaCollectionController extends FeedController abstract class MetaCollectionController extends FeedController
{ {
protected const SLUG = 'collectionsEntry'; protected const SLUG = 'collectionsEntry';
@@ -54,36 +48,17 @@ abstract class MetaCollectionController extends FeedController
protected string $page_title = 'Collections'; protected string $page_title = 'Collections';
abstract public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string; abstract public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string;
abstract public function getCollectionItems(int $owner_id, $collection_id): array;
/**
* @return T[]
*/
abstract public function getCollectionItems(int $owner_id, int $collection_id): array;
/**
* @return T[]
*/
abstract public function getCollectionsByActorId(int $owner_id): array; abstract public function getCollectionsByActorId(int $owner_id): array;
abstract public function getCollectionBy(int $owner_id, int $collection_id);
abstract public function createCollection(int $owner_id, string $name);
/**
* @return T A collection
*/
abstract public function getCollectionBy(int $owner_id, int $collection_id): object;
abstract public function createCollection(int $owner_id, string $name): bool;
/**
* @return ControllerResultType
*/
public function collectionsViewByActorNickname(Request $request, string $nickname): array public function collectionsViewByActorNickname(Request $request, string $nickname): array
{ {
$user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]);
return self::collectionsView($request, $user->getId(), $nickname); return self::collectionsView($request, $user->getId(), $nickname);
} }
/**
* @return ControllerResultType
*/
public function collectionsViewByActorId(Request $request, int $id): array public function collectionsViewByActorId(Request $request, int $id): array
{ {
return self::collectionsView($request, $id, null); return self::collectionsView($request, $id, null);
@@ -95,7 +70,7 @@ abstract class MetaCollectionController extends FeedController
* @param int $id actor id * @param int $id actor id
* @param ?string $nickname actor nickname * @param ?string $nickname actor nickname
* *
* @return ControllerResultType twig template options * @return array twig template options
*/ */
public function collectionsView(Request $request, int $id, ?string $nickname): array public function collectionsView(Request $request, int $id, ?string $nickname): array
{ {
@@ -138,23 +113,34 @@ abstract class MetaCollectionController extends FeedController
// the functions and passing that class to the template. // the functions and passing that class to the template.
// This is suggested at https://web.archive.org/web/20220226132328/https://stackoverflow.com/questions/3595727/twig-pass-function-into-template/50364502 // This is suggested at https://web.archive.org/web/20220226132328/https://stackoverflow.com/questions/3595727/twig-pass-function-into-template/50364502
$fn = new class($id, $nickname, $request, $this, static::SLUG) { $fn = new class($id, $nickname, $request, $this, static::SLUG) {
public function __construct(private int $id, private string $nickname, private Request $request, private object $parent, private string $slug) private $id;
private $nick;
private $request;
private $parent;
private $slug;
public function __construct($id, $nickname, $request, $parent, $slug)
{ {
$this->id = $id;
$this->nick = $nickname;
$this->request = $request;
$this->parent = $parent;
$this->slug = $slug;
} }
// there's already an injected function called path, // there's already an injected function called path,
// that maps to Router::url(name, args), but since // that maps to Router::url(name, args), but since
// I want to preserve nicknames, I think it's better // I want to preserve nicknames, I think it's better
// to use that getUrl function // to use that getUrl function
public function getUrl(int $cid): string public function getUrl($cid)
{ {
return $this->parent->getCollectionUrl($this->id, $this->nickname, $cid); return $this->parent->getCollectionUrl($this->id, $this->nick, $cid);
} }
// There are many collections in this page and we need two // There are many collections in this page and we need two
// forms for each one of them: one form to edit the collection's // forms for each one of them: one form to edit the collection's
// name and another to remove the collection. // name and another to remove the collection.
// creating the edit form // creating the edit form
public function editForm(object $collection): FormView public function editForm($collection)
{ {
$edit = Form::create([ $edit = Form::create([
['name', TextType::class, [ ['name', TextType::class, [
@@ -173,7 +159,7 @@ abstract class MetaCollectionController extends FeedController
]); ]);
$edit->handleRequest($this->request); $edit->handleRequest($this->request);
if ($edit->isSubmitted() && $edit->isValid()) { if ($edit->isSubmitted() && $edit->isValid()) {
$this->parent->setCollectionName($this->id, $this->nickname, $collection, $edit->getData()['name']); $this->parent->setCollectionName($this->id, $this->nick, $collection, $edit->getData()['name']);
DB::flush(); DB::flush();
throw new RedirectException(); throw new RedirectException();
} }
@@ -181,7 +167,7 @@ abstract class MetaCollectionController extends FeedController
} }
// creating the remove form // creating the remove form
public function rmForm(object $collection): FormView public function rmForm($collection)
{ {
$rm = Form::create([ $rm = Form::create([
['remove_' . $collection->getId(), SubmitType::class, [ ['remove_' . $collection->getId(), SubmitType::class, [
@@ -194,7 +180,7 @@ abstract class MetaCollectionController extends FeedController
]); ]);
$rm->handleRequest($this->request); $rm->handleRequest($this->request);
if ($rm->isSubmitted()) { if ($rm->isSubmitted()) {
$this->parent->removeCollection($this->id, $this->nickname, $collection); $this->parent->removeCollection($this->id, $this->nick, $collection);
DB::flush(); DB::flush();
throw new RedirectException(); throw new RedirectException();
} }
@@ -212,18 +198,12 @@ abstract class MetaCollectionController extends FeedController
]; ];
} }
/**
* @return ControllerResultType
*/
public function collectionsEntryViewNotesByNickname(Request $request, string $nickname, int $cid): array public function collectionsEntryViewNotesByNickname(Request $request, string $nickname, int $cid): array
{ {
$user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]);
return self::collectionsEntryViewNotesByActorId($request, $user->getId(), $cid); return self::collectionsEntryViewNotesByActorId($request, $user->getId(), $cid);
} }
/**
* @return ControllerResultType
*/
public function collectionsEntryViewNotesByActorId(Request $request, int $id, int $cid): array public function collectionsEntryViewNotesByActorId(Request $request, int $id, int $cid): array
{ {
$collection = $this->getCollectionBy($id, $cid); $collection = $this->getCollectionBy($id, $cid);

View File

@@ -4,11 +4,6 @@ declare(strict_types = 1);
namespace Component\Collection\Util\Controller; namespace Component\Collection\Util\Controller;
/** class OrderedCollection extends Collection
* @template T
*
* @extends Collection<T>
*/
abstract class OrderedCollection extends Collection
{ {
} }

View File

@@ -31,7 +31,7 @@ declare(strict_types = 1);
namespace Component\Collection\Util; namespace Component\Collection\Util;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
@@ -39,15 +39,11 @@ use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use App\Util\Formatting; use App\Util\Formatting;
use EventResult;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @template T
* */
trait MetaCollectionTrait trait MetaCollectionTrait
{ {
//protected const SLUG = 'collection'; //protected const SLUG = 'collection';
@@ -56,44 +52,40 @@ trait MetaCollectionTrait
/** /**
* create a collection owned by Actor $owner. * create a collection owned by Actor $owner.
* *
* @param Actor $owner The collection's owner * @param Actor $owner The collection's owner
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event * @param array $vars Page vars sent by AppendRightPanelBlock event
* @param string $name Collection's name * @param string $name Collection's name
*/ */
abstract protected function createCollection(Actor $owner, array $vars, string $name): void; abstract protected function createCollection(Actor $owner, array $vars, string $name);
/** /**
* remove item from collections. * remove item from collections.
* *
* @param Actor $owner Current user * @param Actor $owner Current user
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event * @param array $vars Page vars sent by AppendRightPanelBlock event
* @param int[] $items Array of collections's ids to remove the current item from * @param array $items Array of collections's ids to remove the current item from
* @param int[] $collections List of ids of collections owned by $owner * @param array $collections List of ids of collections owned by $owner
*/ */
abstract protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool; abstract protected function removeItem(Actor $owner, array $vars, array $items, array $collections);
/** /**
* add item to collections. * add item to collections.
* *
* @param Actor $owner Current user * @param Actor $owner Current user
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event * @param array $vars Page vars sent by AppendRightPanelBlock event
* @param int[] $items Array of collections's ids to add the current item to * @param array $items Array of collections's ids to add the current item to
* @param int[] $collections List of ids of collections owned by $owner * @param array $collections List of ids of collections owned by $owner
*/ */
abstract protected function addItem(Actor $owner, array $vars, array $items, array $collections): void; abstract protected function addItem(Actor $owner, array $vars, array $items, array $collections);
/** /**
* Check the route to determine whether the widget should be added * Check the route to determine whether the widget should be added
*
* @param array<string, mixed> $vars
*/ */
abstract protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool; abstract protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool;
/** /**
* Get array of collections's owned by $actor * Get array of collections's owned by $actor
* *
* @param Actor $owner Collection's owner * @param Actor $owner Collection's owner
* @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event * @param ?array $vars Page vars sent by AppendRightPanelBlock event
* @param bool $ids_only if true, the function must return only the primary key or each collections * @param bool $ids_only if true, the function must return only the primary key or each collections
*
* @return int[]|T[]
*/ */
abstract protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array; abstract protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array;
@@ -101,11 +93,8 @@ trait MetaCollectionTrait
* Append Collections widget to the right panel. * Append Collections widget to the right panel.
* It's compose of two forms: one to select collections to add * It's compose of two forms: one to select collections to add
* the current item to, and another to create a new collection. * the current item to, and another to create a new collection.
*
* @param array<string, mixed> $vars
* @param string[] $res
*/ */
public function onAppendRightPanelBlock(Request $request, array $vars, array &$res): EventResult public function onAppendRightPanelBlock(Request $request, $vars, &$res): bool
{ {
$user = Common::actor(); $user = Common::actor();
if (\is_null($user)) { if (\is_null($user)) {
@@ -197,10 +186,7 @@ trait MetaCollectionTrait
return Event::next; return Event::next;
} }
/** public function onEndShowStyles(array &$styles, string $route): bool
* @param string[] $styles
*/
public function onEndShowStyles(array &$styles, string $route): EventResult
{ {
$styles[] = 'components/Collection/assets/css/widget.css'; $styles[] = 'components/Collection/assets/css/widget.css';
$styles[] = 'components/Collection/assets/css/pages.css'; $styles[] = 'components/Collection/assets/css/pages.css';

View File

@@ -32,9 +32,6 @@ abstract class Parser
{ {
/** /**
* Merge $parts into $criteria_arr * Merge $parts into $criteria_arr
*
* @param mixed[] $parts
* @param Criteria[] $criteria_arr
*/ */
private static function connectParts(array &$parts, array &$criteria_arr, string $last_op, mixed $eb, bool $force = false): void private static function connectParts(array &$parts, array &$criteria_arr, string $last_op, mixed $eb, bool $force = false): void
{ {

View File

@@ -28,11 +28,11 @@ declare(strict_types = 1);
namespace Component\Conversation\Controller; namespace Component\Conversation\Controller;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
@@ -46,9 +46,6 @@ use Component\Conversation\Entity\ConversationMute;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Conversation extends FeedController class Conversation extends FeedController
{ {
/** /**
@@ -58,10 +55,7 @@ class Conversation extends FeedController
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* *
* @return ControllerResultType Array containing keys: 'notes' (all known * @return array Array containing keys: 'notes' (all known notes in the given Conversation), 'should_format' (boolean, stating if onFormatNoteList events may or not format given notes), 'page_title' (used as the title header)
* notes in the given Conversation), 'should_format' (boolean, stating if
* onFormatNoteList events may or not format given notes), 'page_title'
* (used as the title header)
*/ */
public function showConversation(Request $request, int $conversation_id): array public function showConversation(Request $request, int $conversation_id): array
{ {
@@ -89,7 +83,7 @@ class Conversation extends FeedController
* @throws NoSuchNoteException * @throws NoSuchNoteException
* @throws ServerException * @throws ServerException
* *
* @return ControllerResultType * @return array
*/ */
public function addReply(Request $request) public function addReply(Request $request)
{ {
@@ -109,7 +103,7 @@ class Conversation extends FeedController
* @throws \App\Util\Exception\RedirectException * @throws \App\Util\Exception\RedirectException
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* *
* @return ControllerResultType Array containing templating where the form is to be rendered, and the form itself * @return array Array containing templating where the form is to be rendered, and the form itself
*/ */
public function muteConversation(Request $request, int $conversation_id) public function muteConversation(Request $request, int $conversation_id)
{ {

View File

@@ -28,11 +28,12 @@ declare(strict_types = 1);
namespace Component\Conversation; namespace Component\Conversation;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note; use App\Entity\Note;
@@ -40,13 +41,12 @@ use App\Util\Common;
use App\Util\Formatting; use App\Util\Formatting;
use Component\Conversation\Entity\Conversation as ConversationEntity; use Component\Conversation\Entity\Conversation as ConversationEntity;
use Component\Conversation\Entity\ConversationMute; use Component\Conversation\Entity\ConversationMute;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Conversation extends Component class Conversation extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect('conversation', '/conversation/{conversation_id<\d+>}', [Controller\Conversation::class, 'showConversation']); $r->connect('conversation', '/conversation/{conversation_id<\d+>}', [Controller\Conversation::class, 'showConversation']);
$r->connect('conversation_mute', '/conversation/{conversation_id<\d+>}/mute', [Controller\Conversation::class, 'muteConversation']); $r->connect('conversation_mute', '/conversation/{conversation_id<\d+>}/mute', [Controller\Conversation::class, 'muteConversation']);
@@ -95,17 +95,17 @@ class Conversation extends Component
* HTML rendering event that adds a reply link as a note * HTML rendering event that adds a reply link as a note
* action, if a user is logged in. * action, if a user is logged in.
* *
* @param \App\Entity\Note $note The Note being rendered * @param \App\Entity\Note $note The Note being rendered
* @param array{url: string, title: string, classes: string, id: string} $actions * @param array $actions Contains keys 'url' (linking 'conversation_reply_to'
* Contains keys 'url' (linking 'conversation_reply_to' route), * route), 'title' (used as title for aforementioned url),
* 'title' (used as title for aforementioned url), 'classes' (CSS styling * 'classes' (CSS styling classes used to visually inform the user of action context),
* classes used to visually inform the user of action context), 'id' (HTML * 'id' (HTML markup id used to redirect user to this anchor upon performing the action)
* markup id used to redirect user to this anchor upon performing the
* action)
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
*
* @return bool EventHook
*/ */
public function onAddNoteActions(Request $request, Note $note, array &$actions): EventResult public function onAddNoteActions(Request $request, Note $note, array &$actions): bool
{ {
if (\is_null(Common::user())) { if (\is_null(Common::user())) {
return Event::next; return Event::next;
@@ -140,13 +140,12 @@ class Conversation extends Component
/** /**
* Append on note information about user actions. * Append on note information about user actions.
* *
* @param array<string, mixed> $vars Contains information related to Note currently being rendered * @param array $vars Contains information related to Note currently being rendered
* @param array{actors: Actor[], action: string} $result * @param array $result Contains keys 'actors', and 'action'. Needed to construct a string, stating who ($result['actors']), has already performed a reply ($result['action']), in the given Note (vars['note'])
*cContains keys 'actors', and 'action'. Needed to construct a string, *
* stating who ($result['actors']), has already performed a reply * @return bool EventHook
* ($result['action']), in the given Note (vars['note'])
*/ */
public function onAppendCardNote(array $vars, array &$result): EventResult public function onAppendCardNote(array $vars, array &$result): bool
{ {
if (str_contains($vars['request']->getPathInfo(), 'conversation')) { if (str_contains($vars['request']->getPathInfo(), 'conversation')) {
return Event::next; return Event::next;
@@ -196,8 +195,10 @@ class Conversation extends Component
* *
* @param \App\Entity\Actor $actor The Actor currently attempting to post a Note * @param \App\Entity\Actor $actor The Actor currently attempting to post a Note
* @param null|\App\Entity\Actor $context_actor The 'owner' of the current route (e.g. Group or Actor), used to target it * @param null|\App\Entity\Actor $context_actor The 'owner' of the current route (e.g. Group or Actor), used to target it
*
* @return bool EventHook
*/ */
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): EventResult public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): bool
{ {
$to_note_id = $this->getReplyToIdFromRequest($request); $to_note_id = $this->getReplyToIdFromRequest($request);
if (!\is_null($to_note_id)) { if (!\is_null($to_note_id)) {
@@ -211,12 +212,14 @@ class Conversation extends Component
/** /**
* Posting event to add extra information to Component\Posting form data * Posting event to add extra information to Component\Posting form data
* *
* @param array{reply_to_id: int} $data Transport data to be filled with reply_to_id * @param array $data Transport data to be filled with reply_to_id
* *
* @throws \App\Util\Exception\ClientException * @throws \App\Util\Exception\ClientException
* @throws \App\Util\Exception\NoSuchNoteException * @throws \App\Util\Exception\NoSuchNoteException
*
* @return bool EventHook
*/ */
public function onPostingModifyData(Request $request, Actor $actor, array &$data): EventResult public function onPostingModifyData(Request $request, Actor $actor, array &$data): bool
{ {
$to_note_id = $this->getReplyToIdFromRequest($request); $to_note_id = $this->getReplyToIdFromRequest($request);
if (!\is_null($to_note_id)) { if (!\is_null($to_note_id)) {
@@ -229,10 +232,8 @@ class Conversation extends Component
/** /**
* Add minimal Note card to RightPanel template * Add minimal Note card to RightPanel template
*
* @param string[] $elements
*/ */
public function onPrependPostingForm(Request $request, array &$elements): EventResult public function onPrependPostingForm(Request $request, array &$elements): bool
{ {
$elements[] = Formatting::twigRenderFile('cards/blocks/note_compact_wrapper.html.twig', ['note' => Note::getById((int) $request->query->get('reply_to_id'))]); $elements[] = Formatting::twigRenderFile('cards/blocks/note_compact_wrapper.html.twig', ['note' => Note::getById((int) $request->query->get('reply_to_id'))]);
return Event::next; return Event::next;
@@ -244,8 +245,10 @@ class Conversation extends Component
* *
* @param \App\Entity\Note $note Note being deleted * @param \App\Entity\Note $note Note being deleted
* @param \App\Entity\Actor $actor Actor that performed the delete action * @param \App\Entity\Actor $actor Actor that performed the delete action
*
* @return bool EventHook
*/ */
public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult public function onNoteDeleteRelated(Note &$note, Actor $actor): bool
{ {
// Ensure we have the most up to date replies // Ensure we have the most up to date replies
Cache::delete(Note::cacheKeys($note->getId())['replies']); Cache::delete(Note::cacheKeys($note->getId())['replies']);
@@ -257,14 +260,14 @@ class Conversation extends Component
/** /**
* Adds extra actions related to Conversation Component, that act upon/from the given Note. * Adds extra actions related to Conversation Component, that act upon/from the given Note.
* *
* @param \App\Entity\Note $note Current Note being rendered * @param \App\Entity\Note $note Current Note being rendered
* @param array{url: string, title: string, classes?: string} $actions Containing 'url' (Controller connected * @param array $actions Containing 'url' (Controller connected route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if needed)
* route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if
* needed)
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
*
* @return bool EventHook
*/ */
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): EventResult public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): bool
{ {
if (\is_null($user = Common::user())) { if (\is_null($user = Common::user())) {
return Event::next; return Event::next;
@@ -297,8 +300,10 @@ class Conversation extends Component
* Prevents new Notifications to appear for muted conversations * Prevents new Notifications to appear for muted conversations
* *
* @param Activity $activity Notification Activity * @param Activity $activity Notification Activity
*
* @return bool EventHook
*/ */
public function onNewNotificationShould(Activity $activity, Actor $actor): EventResult public function onNewNotificationShould(Activity $activity, Actor $actor): bool
{ {
if ($activity->getObjectType() === 'note' && ConversationMute::isMuted($activity, $actor)) { if ($activity->getObjectType() === 'note' && ConversationMute::isMuted($activity, $actor)) {
return Event::stop; return Event::stop;

View File

@@ -23,9 +23,9 @@ declare(strict_types = 1);
namespace Component\Conversation\Entity; namespace Component\Conversation\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router; use App\Core\Router\Router;
/** /**
* Entity class for Conversations * Entity class for Conversations

View File

@@ -24,7 +24,7 @@ declare(strict_types = 1);
namespace Component\Conversation\Entity; namespace Component\Conversation\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;

View File

@@ -41,15 +41,10 @@ use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController; use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Feeds extends FeedController class Feeds extends FeedController
{ {
/** /**
* The Planet feed represents every local post. Which is what this instance has to share with the universe. * The Planet feed represents every local post. Which is what this instance has to share with the universe.
*
* @return ControllerResultType
*/ */
public function public(Request $request): array public function public(Request $request): array
{ {
@@ -65,8 +60,6 @@ class Feeds extends FeedController
/** /**
* The Home feed represents everything that concerns a certain actor (its subscriptions) * The Home feed represents everything that concerns a certain actor (its subscriptions)
*
* @return ControllerResultType
*/ */
public function home(Request $request): array public function home(Request $request): array
{ {

View File

@@ -25,13 +25,12 @@ namespace Component\Feed;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use Component\Feed\Controller as C; use Component\Feed\Controller as C;
use EventResult;
class Feed extends Component class Feed extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect('feed_public', '/feed/public', [C\Feeds::class, 'public']); $r->connect('feed_public', '/feed/public', [C\Feeds::class, 'public']);
$r->connect('feed_home', '/feed/home', [C\Feeds::class, 'home']); $r->connect('feed_home', '/feed/home', [C\Feeds::class, 'home']);

View File

@@ -23,7 +23,7 @@ declare(strict_types = 1);
namespace Component\Feed\tests\Controller; namespace Component\Feed\tests\Controller;
use App\Core\Router; use App\Core\Router\Router;
use App\Util\GNUsocialTestCase; use App\Util\GNUsocialTestCase;
use Component\Feed\Controller\Feeds; use Component\Feed\Controller\Feeds;
use Jchook\AssertThrows\AssertThrows; use Jchook\AssertThrows\AssertThrows;

View File

@@ -34,7 +34,7 @@ declare(strict_types = 1);
namespace Component\FreeNetwork\Controller; namespace Component\FreeNetwork\Controller;
use App\Core\DB; use App\Core\DB\DB;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Util\Common; use App\Util\Common;
use Component\Collection\Util\Controller\FeedController; use Component\Collection\Util\Controller\FeedController;

View File

@@ -32,7 +32,7 @@ declare(strict_types = 1);
namespace Component\FreeNetwork\Entity; namespace Component\FreeNetwork\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Actor; use App\Entity\Actor;
use Component\FreeNetwork\Util\Discovery; use Component\FreeNetwork\Util\Discovery;

View File

@@ -21,14 +21,15 @@ declare(strict_types = 1);
namespace Component\FreeNetwork; namespace Component\FreeNetwork;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;
use App\Core\HTTPClient; use App\Core\HTTPClient;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser; use App\Entity\LocalUser;
@@ -54,7 +55,6 @@ use Component\FreeNetwork\Util\WebfingerResource;
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor; use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor;
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote; use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use EventResult;
use Exception; use Exception;
use const PREG_SET_ORDER; use const PREG_SET_ORDER;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
@@ -80,13 +80,13 @@ class FreeNetwork extends Component
public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize'; public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize';
private static array $protocols = []; private static array $protocols = [];
public function onInitializeComponent(): EventResult public function onInitializeComponent(): bool
{ {
Event::handle('AddFreeNetworkProtocol', [&self::$protocols]); Event::handle('AddFreeNetworkProtocol', [&self::$protocols]);
return Event::next; return Event::next;
} }
public function onAddRoute(Router $m): EventResult public function onAddRoute(RouteLoader $m): bool
{ {
// Feeds // Feeds
$m->connect('feed_network', '/feed/network', [Feeds::class, 'network']); $m->connect('feed_network', '/feed/network', [Feeds::class, 'network']);
@@ -112,7 +112,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering)
{ {
DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_network'), 'route' => $route, 'title' => _m('Meteorites'), 'ordering' => $ordering++])); DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_network'), 'route' => $route, 'title' => _m('Meteorites'), 'ordering' => $ordering++]));
DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_clique'), 'route' => $route, 'title' => _m('Planetary System'), 'ordering' => $ordering++])); DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_clique'), 'route' => $route, 'title' => _m('Planetary System'), 'ordering' => $ordering++]));
@@ -120,7 +120,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onStartGetProfileAcctUri(Actor $profile, &$acct): EventResult public function onStartGetProfileAcctUri(Actor $profile, &$acct): bool
{ {
$wfr = new WebFingerResourceActor($profile); $wfr = new WebFingerResourceActor($profile);
try { try {
@@ -148,7 +148,7 @@ class FreeNetwork extends Component
* @throws NoSuchActorException * @throws NoSuchActorException
* @throws ServerException * @throws ServerException
*/ */
public function onEndGetWebFingerResource(string $resource, ?WebfingerResource &$target = null, array $args = []): EventResult public function onEndGetWebFingerResource(string $resource, ?WebfingerResource &$target = null, array $args = []): bool
{ {
// * Either we didn't find the profile, then we want to make // * Either we didn't find the profile, then we want to make
// the $profile variable null for clarity. // the $profile variable null for clarity.
@@ -224,7 +224,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onStartHostMetaLinks(array &$links): EventResult public function onStartHostMetaLinks(array &$links): bool
{ {
foreach (Discovery::supportedMimeTypes() as $type) { foreach (Discovery::supportedMimeTypes() as $type) {
$links[] = new XML_XRD_Element_Link( $links[] = new XML_XRD_Element_Link(
@@ -244,10 +244,8 @@ class FreeNetwork extends Component
/** /**
* Add a link header for LRDD Discovery * Add a link header for LRDD Discovery
*
* @param mixed $action
*/ */
public function onStartShowHTML($action): EventResult public function onStartShowHTML($action): bool
{ {
if ($action instanceof ShowstreamAction) { if ($action instanceof ShowstreamAction) {
$resource = $action->getTarget()->getUri(); $resource = $action->getTarget()->getUri();
@@ -260,13 +258,13 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onStartDiscoveryMethodRegistration(Discovery $disco): EventResult public function onStartDiscoveryMethodRegistration(Discovery $disco): bool
{ {
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodWebfinger'); $disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodWebfinger');
return Event::next; return Event::next;
} }
public function onEndDiscoveryMethodRegistration(Discovery $disco): EventResult public function onEndDiscoveryMethodRegistration(Discovery $disco): bool
{ {
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodHostMeta'); $disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodHostMeta');
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodLinkHeader'); $disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodLinkHeader');
@@ -278,7 +276,7 @@ class FreeNetwork extends Component
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*/ */
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?Response &$response = null): EventResult public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?Response &$response = null): bool
{ {
if (!\in_array($route, ['freenetwork_hostmeta', 'freenetwork_hostmeta_format', 'freenetwork_webfinger', 'freenetwork_webfinger_format', 'freenetwork_ownerxrd'])) { if (!\in_array($route, ['freenetwork_hostmeta', 'freenetwork_hostmeta_format', 'freenetwork_webfinger', 'freenetwork_webfinger_format', 'freenetwork_ownerxrd'])) {
return Event::next; return Event::next;
@@ -346,7 +344,6 @@ class FreeNetwork extends Component
* @param string $preMention Character(s) that signals a mention ('@', '!'...) * @param string $preMention Character(s) that signals a mention ('@', '!'...)
* *
* @return array the matching URLs (without @ or acct:) and each respective position in the given string * @return array the matching URLs (without @ or acct:) and each respective position in the given string
*
* @example.com/mublog/user * @example.com/mublog/user
*/ */
public static function extractUrlMentions(string $text, string $preMention = '@'): array public static function extractUrlMentions(string $text, string $preMention = '@'): array
@@ -378,10 +375,9 @@ class FreeNetwork extends Component
* @param $mentions * @param $mentions
* *
* @return bool hook return value * @return bool hook return value
*
* @example.com/mublog/user * @example.com/mublog/user
*/ */
public function onEndFindMentions(Actor $sender, string $text, array &$mentions): EventResult public function onEndFindMentions(Actor $sender, string $text, array &$mentions): bool
{ {
$matches = []; $matches = [];
@@ -500,9 +496,6 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
/**
* @param Actor[] $targets
*/
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
{ {
foreach (self::$protocols as $protocol) { foreach (self::$protocols as $protocol) {
@@ -524,11 +517,8 @@ class FreeNetwork extends Component
/** /**
* Add fediverse: query expression * Add fediverse: query expression
* // TODO: adding WebFinger would probably be nice * // TODO: adding WebFinger would probably be nice
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool
{ {
if (Formatting::startsWith($term, ['fediverse:'])) { if (Formatting::startsWith($term, ['fediverse:'])) {
foreach (self::$protocols as $protocol) { foreach (self::$protocols as $protocol) {
@@ -541,7 +531,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onPluginVersion(array &$versions): EventResult public function onPluginVersion(array &$versions): bool
{ {
$versions[] = [ $versions[] = [
'name' => 'WebFinger', 'name' => 'WebFinger',

View File

@@ -6,7 +6,7 @@ namespace Component\FreeNetwork\Util\WebfingerResource;
use App\Core\Event; use App\Core\Event;
use App\Core\Log; use App\Core\Log;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
use Component\FreeNetwork\Exception\WebfingerReconstructionException; use Component\FreeNetwork\Exception\WebfingerReconstructionException;

View File

@@ -26,7 +26,7 @@ namespace Component\Group\Controller;
use App\Core\ActorLocalRoles; use App\Core\ActorLocalRoles;
use App\Core\Cache; use App\Core\Cache;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
@@ -61,8 +61,6 @@ class Group extends Controller
* *
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupCreate(Request $request): array public function groupCreate(Request $request): array
{ {
@@ -91,8 +89,6 @@ class Group extends Controller
* @throws NicknameTooLongException * @throws NicknameTooLongException
* @throws NotFoundException * @throws NotFoundException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupSettings(Request $request, int $id): array public function groupSettings(Request $request, int $id): array
{ {

View File

@@ -24,10 +24,10 @@ declare(strict_types = 1);
namespace Component\Group\Controller; namespace Component\Group\Controller;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity as E; use App\Entity as E;
use App\Util\Common; use App\Util\Common;
@@ -40,15 +40,10 @@ use Component\Subscription\Entity\ActorSubscription;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class GroupFeed extends FeedController class GroupFeed extends FeedController
{ {
/** /**
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupView(Request $request, Actor $group): array public function groupView(Request $request, Actor $group): array
{ {
@@ -85,7 +80,6 @@ class GroupFeed extends FeedController
WHERE act.object_type = 'note' AND act.id IN WHERE act.object_type = 'note' AND act.id IN
(SELECT att.activity_id FROM \Component\Notification\Entity\Notification AS att WHERE att.target_id = :id) (SELECT att.activity_id FROM \Component\Notification\Entity\Notification AS att WHERE att.target_id = :id)
) )
ORDER BY n.created DESC
EOF, ['id' => $group->getId()]); EOF, ['id' => $group->getId()]);
return [ return [
@@ -101,8 +95,6 @@ class GroupFeed extends FeedController
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupViewId(Request $request, int $id): array public function groupViewId(Request $request, int $id): array
{ {
@@ -126,8 +118,6 @@ class GroupFeed extends FeedController
* *
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupViewNickname(Request $request, string $nickname): array public function groupViewNickname(Request $request, string $nickname): array
{ {

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Group\Entity; namespace Component\Group\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\Exception\NicknameEmptyException; use App\Util\Exception\NicknameEmptyException;

View File

@@ -24,7 +24,8 @@ namespace Component\Group;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
@@ -33,12 +34,11 @@ use App\Util\Nickname;
use Component\Group\Controller as C; use Component\Group\Controller as C;
use Component\Group\Entity\LocalGroup; use Component\Group\Entity\LocalGroup;
use Component\Notification\Notification; use Component\Notification\Notification;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Group extends Component class Group extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect(id: 'group_actor_view_id', uri_path: '/group/{id<\d+>}', target: [C\GroupFeed::class, 'groupViewId']); $r->connect(id: 'group_actor_view_id', uri_path: '/group/{id<\d+>}', target: [C\GroupFeed::class, 'groupViewId']);
$r->connect(id: 'group_actor_view_nickname', uri_path: '/!{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\GroupFeed::class, 'groupViewNickname']); $r->connect(id: 'group_actor_view_nickname', uri_path: '/!{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\GroupFeed::class, 'groupViewNickname']);
@@ -50,20 +50,13 @@ class Group extends Component
/** /**
* Enqueues a notification for an Actor (such as person or group) which means * Enqueues a notification for an Actor (such as person or group) which means
* it shows up in their home feed and such. * it shows up in their home feed and such.
*
* @param Actor[] $targets
*/ */
public function onNewNotificationStart(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): EventResult public function onNewNotificationWithTargets(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): bool
{ {
foreach ($targets as $target) { foreach ($targets as $target) {
if ($target->isGroup()) { if ($target->isGroup()) {
// The Group announces to its subscribers // The Group announces to its subscribers
Notification::notify( Notification::notify($target, $activity, $target->getSubscribers(), $reason);
sender: $target,
activity: $activity,
targets: $target->getSubscribers(),
reason: $reason,
);
} }
} }
@@ -72,11 +65,8 @@ class Group extends Component
/** /**
* Add an <a href=group_actor_settings> to the profile card for groups, if the current actor can access them * Add an <a href=group_actor_settings> to the profile card for groups, if the current actor can access them
*
* @param array<string, mixed> $vars
* @param string[] $res
*/ */
public function onAppendCardProfile(array $vars, array &$res): EventResult public function onAppendCardProfile(array $vars, array &$res): bool
{ {
$actor = Common::actor(); $actor = Common::actor();
$group = $vars['actor']; $group = $vars['actor'];
@@ -85,6 +75,7 @@ class Group extends Component
$url = Router::url('group_actor_settings', ['id' => $group->getId()]); $url = Router::url('group_actor_settings', ['id' => $group->getId()]);
$res[] = HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => _m('Edit group settings'), 'class' => 'profile-extra-actions'], _m('Group settings')]]); $res[] = HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => _m('Edit group settings'), 'class' => 'profile-extra-actions'], _m('Group settings')]]);
} }
$res[] = HTML::html(['a' => ['attrs' => ['href' => Router::url('blog_post', ['in' => $group->getId()]), 'title' => _m('Make a new blog post'), 'class' => 'profile-extra-actions'], _m('Post in blog')]]);
} }
return Event::next; return Event::next;
} }
@@ -108,16 +99,13 @@ class Group extends Component
case 'group_actor_view_nickname': case 'group_actor_view_nickname':
return LocalGroup::getActorByNickname($identifier); return LocalGroup::getActorByNickname($identifier);
case 'group_actor_view_id': case 'group_actor_view_id':
return Actor::getById((int) $identifier); return Actor::getById($identifier);
} }
} }
return null; return null;
} }
/** public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): bool
* @param Actor[] $targets
*/
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): EventResult
{ {
$group = $this->getGroupFromContext($request); $group = $this->getGroupFromContext($request);
if (!\is_null($group)) { if (!\is_null($group)) {
@@ -136,7 +124,7 @@ class Group extends Component
* *
* @param null|Actor $context_actor Actor group, if current route is part of an existing Group set of routes * @param null|Actor $context_actor Actor group, if current route is part of an existing Group set of routes
*/ */
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): EventResult public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): bool
{ {
$ctx = $this->getGroupFromContext($request); $ctx = $this->getGroupFromContext($request);
if (!\is_null($ctx)) { if (!\is_null($ctx)) {

View File

@@ -7,7 +7,7 @@
<h1>Settings</h1> <h1>Settings</h1>
<ul> <ul>
<li> <li>
{% set profile_tabs = [{'title': 'Personal Info', 'desc': 'Nickname, Homepage, Bio and more.', 'id': 'settings-personal-info', 'form': personal_info_form}] %} {% set profile_tabs = [{'title': 'Personal Info', 'desc': 'Nickname, Homepage, Bio, Self Tags and more.', 'id': 'settings-personal-info', 'form': personal_info_form}] %}
{% set profile_tabs = profile_tabs|merge(handle_event('PopulateSettingsTabs', app.request, 'profile')) %} {% set profile_tabs = profile_tabs|merge(handle_event('PopulateSettingsTabs', app.request, 'profile')) %}
{{ macros.settings_details_container('Profile', 'Personal Information, Avatar and Profile', 'settings-profile-details', profile_tabs, _context) }} {{ macros.settings_details_container('Profile', 'Personal Information, Avatar and Profile', 'settings-profile-details', profile_tabs, _context) }}
</li> </li>

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Group\tests\Entity; namespace Component\Group\tests\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\GNUsocialTestCase; use App\Util\GNUsocialTestCase;
use Component\Group\Entity\LocalGroup; use Component\Group\Entity\LocalGroup;

View File

@@ -25,7 +25,7 @@ namespace Component\Language\Controller;
use App\Core\Cache; use App\Core\Cache;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Util\Common; use App\Util\Common;
@@ -100,8 +100,6 @@ class Language extends Controller
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function sortLanguages(Request $request): array public function sortLanguages(Request $request): array
{ {

View File

@@ -24,7 +24,7 @@ declare(strict_types = 1);
namespace Component\Language\Entity; namespace Component\Language\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser; use App\Entity\LocalUser;
@@ -119,9 +119,6 @@ class ActorLanguage extends Entity
) ?: [Language::getByLocale(Common::config('site', 'language'))]; ) ?: [Language::getByLocale(Common::config('site', 'language'))];
} }
/**
* @return int[]
*/
public static function getActorRelatedLanguagesIds(Actor $actor): array public static function getActorRelatedLanguagesIds(Actor $actor): array
{ {
return Cache::getList( return Cache::getList(

View File

@@ -24,7 +24,7 @@ declare(strict_types = 1);
namespace Component\Language\Entity; namespace Component\Language\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Entity\Actor; use App\Entity\Actor;
@@ -116,7 +116,7 @@ class Language extends Entity
return Cache::getHashMapKey( return Cache::getHashMapKey(
map_key: 'languages-id', map_key: 'languages-id',
key: (string) $id, key: (string) $id,
calculate_map: fn () => F\reindex(DB::dql('SELECT l FROM \Component\Language\Entity\Language AS l'), fn (self $l) => (string) $l->getId()), calculate_map: fn () => F\reindex(DB::dql('select l from language l'), fn (self $l) => (string) $l->getId()),
); );
} }
@@ -125,7 +125,7 @@ class Language extends Entity
return Cache::getHashMapKey( return Cache::getHashMapKey(
'languages', 'languages',
$locale, $locale,
calculate_map: fn () => F\reindex(DB::dql('SELECT l FROM \Component\Language\Entity\Language AS l'), fn (self $l) => $l->getLocale()), calculate_map: fn () => F\reindex(DB::dql('select l from language l'), fn (self $l) => $l->getLocale()),
); );
} }
@@ -134,21 +134,16 @@ class Language extends Entity
return self::getById($note->getLanguageId()); return self::getById($note->getLanguageId());
} }
/**
* @return array<string, string>
*/
public static function getLanguageChoices(): array public static function getLanguageChoices(): array
{ {
$langs = Cache::getHashMap( $langs = Cache::getHashMap(
'languages', 'languages',
fn () => F\reindex(DB::dql('SELECT l FROM \Component\Language\Entity\Language AS l'), fn (self $l) => $l->getLocale()), fn () => F\reindex(DB::dql('select l from language l'), fn (self $l) => $l->getLocale()),
); );
return array_merge(...F\map(array_values($langs), fn ($l) => $l->toChoiceFormat())); return array_merge(...F\map(array_values($langs), fn ($l) => $l->toChoiceFormat()));
} }
/**
* @return array<string, string> */
public function toChoiceFormat(): array public function toChoiceFormat(): array
{ {
return [_m($this->getLongDisplay()) => $this->getLocale()]; return [_m($this->getLongDisplay()) => $this->getLocale()];
@@ -157,8 +152,6 @@ class Language extends Entity
/** /**
* Get all the available languages as well as the languages $actor * Get all the available languages as well as the languages $actor
* prefers and are appropriate for posting in/to $context_actor * prefers and are appropriate for posting in/to $context_actor
*
* @return array{array<string, string>, array<string, string>}
*/ */
public static function getSortedLanguageChoices(?Actor $actor, ?Actor $context_actor, ?bool $use_short_display): array public static function getSortedLanguageChoices(?Actor $actor, ?Actor $context_actor, ?bool $use_short_display): array
{ {

View File

@@ -23,7 +23,7 @@ namespace Component\Language;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Formatting; use App\Util\Formatting;
@@ -33,22 +33,18 @@ use Component\Language\Entity\ActorLanguage;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Language extends Component class Language extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect('settings_sort_languages', '/settings/sort_languages', [C\Language::class, 'sortLanguages']); $r->connect('settings_sort_languages', '/settings/sort_languages', [C\Language::class, 'sortLanguages']);
return Event::next; return Event::next;
} }
/** public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): bool
* @param Note[] $notes
*/
public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): EventResult
{ {
if (\is_null($actor)) { if (\is_null($actor)) {
return Event::next; return Event::next;
@@ -63,11 +59,8 @@ class Language extends Component
/** /**
* Populate $note_expr or $actor_expr with an expression to match a language * Populate $note_expr or $actor_expr with an expression to match a language
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool
{ {
$search_term = str_contains($term, ':') ? explode(':', $term)[1] : $term; $search_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;
@@ -110,7 +103,7 @@ class Language extends Component
return Event::next; return Event::next;
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
{ {
$note_aliases = $note_qb->getAllAliases(); $note_aliases = $note_qb->getAllAliases();
if (!\in_array('note_language', $note_aliases)) { if (!\in_array('note_language', $note_aliases)) {
@@ -123,7 +116,7 @@ class Language extends Component
$note_qb->leftJoin('Component\Language\Entity\Language', 'note_actor_language', Expr\Join::WITH, 'note_actor_language.id = actor_language.language_id'); $note_qb->leftJoin('Component\Language\Entity\Language', 'note_actor_language', Expr\Join::WITH, 'note_actor_language.id = actor_language.language_id');
} }
$actor_aliases = $actor_qb->getAllAliases(); $actor_aliases = $note_qb->getAllAliases();
if (!\in_array('actor_language', $actor_aliases)) { if (!\in_array('actor_language', $actor_aliases)) {
$actor_qb->leftJoin('Component\Language\Entity\ActorLanguage', 'actor_language', Expr\Join::WITH, 'actor.id = actor_language.actor_id'); $actor_qb->leftJoin('Component\Language\Entity\ActorLanguage', 'actor_language', Expr\Join::WITH, 'actor.id = actor_language.actor_id');
} }

View File

@@ -25,10 +25,10 @@ namespace Component\LeftPanel\Controller;
use App\Core\Cache; use App\Core\Cache;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Feed; use App\Entity\Feed;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
@@ -104,6 +104,7 @@ class EditFeeds extends Controller
$feed->setUrl($fd[$md5 . '-url']); $feed->setUrl($fd[$md5 . '-url']);
$feed->setOrdering($fd[$md5 . '-order']); $feed->setOrdering($fd[$md5 . '-order']);
$feed->setTitle($fd[$md5 . '-title']); $feed->setTitle($fd[$md5 . '-title']);
DB::merge($feed);
} }
DB::flush(); DB::flush();
Cache::delete($key); Cache::delete($key);
@@ -118,6 +119,7 @@ class EditFeeds extends Controller
/** @var SubmitButton $remove_button */ /** @var SubmitButton $remove_button */
$remove_button = $form->get($remove_id); $remove_button = $form->get($remove_id);
if ($remove_button->isClicked()) { if ($remove_button->isClicked()) {
// @phpstan-ignore-next-line -- Doesn't quite understand that _this_ $opts for the current $form_definitions does have 'data'
DB::remove(DB::getReference('feed', ['actor_id' => $user->getId(), 'url' => $opts['data']])); DB::remove(DB::getReference('feed', ['actor_id' => $user->getId(), 'url' => $opts['data']]));
DB::flush(); DB::flush();
Cache::delete($key); Cache::delete($key);

View File

@@ -22,34 +22,32 @@ declare(strict_types = 1);
namespace Component\LeftPanel; namespace Component\LeftPanel;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Feed; use App\Entity\Feed;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\NotFoundException; use App\Util\Exception\NotFoundException;
use Component\LeftPanel\Controller as C; use Component\LeftPanel\Controller as C;
use EventResult;
class LeftPanel extends Component class LeftPanel extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect('edit_feeds', '/edit-feeds', C\EditFeeds::class); $r->connect('edit_feeds', '/edit-feeds', C\EditFeeds::class);
return Event::next; return Event::next;
} }
/** /**
* @param array<string, string> $route_params
*
* @throws \App\Util\Exception\DuplicateFoundException * @throws \App\Util\Exception\DuplicateFoundException
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
*/ */
public function onAppendFeed(Actor $actor, string $title, string $route, array $route_params): EventResult public function onAppendFeed(Actor $actor, string $title, string $route, array $route_params): bool
{ {
$cache_key = Feed::cacheKey($actor); $cache_key = Feed::cacheKey($actor);
$feeds = Feed::getFeeds($actor); $feeds = Feed::getFeeds($actor);

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Link\Entity; namespace Component\Link\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;

View File

@@ -21,7 +21,7 @@ declare(strict_types = 1);
namespace Component\Link\Entity; namespace Component\Link\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Event; use App\Core\Event;
use DateTimeInterface; use DateTimeInterface;
@@ -85,15 +85,15 @@ class NoteToLink extends Entity
* properties of $obj with the associative array $args. Doesn't * properties of $obj with the associative array $args. Doesn't
* persist the result * persist the result
* *
* @param (array{link_id: int, note_id: int} & array<string, mixed>) $args * @param null|mixed $obj
*/ */
public static function create(array $args, bool $_delegated_call = false): static public static function create(array $args, $obj = null)
{ {
$link = DB::find('link', ['id' => $args['link_id']]); $link = DB::find('link', ['id' => $args['link_id']]);
$note = DB::find('note', ['id' => $args['note_id']]); $note = DB::find('note', ['id' => $args['note_id']]);
Event::handle('NewLinkFromNote', [$link, $note]); Event::handle('NewLinkFromNote', [$link, $note]);
$obj = new self(); $obj = new self();
return parent::createOrUpdate(obj: $obj, args: $args); return parent::create($args, $obj);
} }
public static function removeWhereNoteId(int $note_id): mixed public static function removeWhereNoteId(int $note_id): mixed

View File

@@ -23,7 +23,7 @@ declare(strict_types = 1);
namespace Component\Link; namespace Component\Link;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Entity\Actor; use App\Entity\Actor;
@@ -31,33 +31,14 @@ use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\HTML; use App\Util\HTML;
use Component\Link\Entity\NoteToLink; use Component\Link\Entity\NoteToLink;
use EventResult;
use InvalidArgumentException; use InvalidArgumentException;
class Link extends Component class Link extends Component
{ {
/**
* Note that this persists both a Link and a NoteToLink
*
* @return array{ link: ?Entity\Link, note_to_link: ?NoteToLink }
*/
public static function maybeCreateLink(string $url, int $note_id): array
{
try {
$link = Entity\Link::getOrCreate($url);
DB::persist($note_link = NoteToLink::create(['link_id' => $link->getId(), 'note_id' => $note_id]));
return ['link' => $link, 'note_to_link' => $note_link];
} catch (InvalidArgumentException) {
return ['link' => null, 'note_to_link' => null];
}
}
/** /**
* Extract URLs from $content and create the appropriate Link and NoteToLink entities * Extract URLs from $content and create the appropriate Link and NoteToLink entities
*
* @param array{ignoreLinks?: string[]} $process_note_content_extra_args
*/ */
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): EventResult public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): bool
{ {
$ignore = $process_note_content_extra_args['ignoreLinks'] ?? []; $ignore = $process_note_content_extra_args['ignoreLinks'] ?? [];
if (Common::config('attachments', 'process_links')) { if (Common::config('attachments', 'process_links')) {
@@ -68,13 +49,18 @@ class Link extends Component
if (\in_array($match, $ignore)) { if (\in_array($match, $ignore)) {
continue; continue;
} }
self::maybeCreateLink($match, $note->getId()); try {
$link_id = Entity\Link::getOrCreate($match)->getId();
DB::persist(NoteToLink::create(['link_id' => $link_id, 'note_id' => $note->getId()]));
} catch (InvalidArgumentException) {
continue;
}
} }
} }
return Event::next; return Event::next;
} }
public function onRenderPlainTextNoteContent(string &$text): EventResult public function onRenderPlainTextNoteContent(string &$text): bool
{ {
$text = $this->replaceURLs($text); $text = $this->replaceURLs($text);
return Event::next; return Event::next;
@@ -152,12 +138,7 @@ class Link extends Component
public const URL_SCHEME_NO_DOMAIN = 4; public const URL_SCHEME_NO_DOMAIN = 4;
public const URL_SCHEME_COLON_COORDINATES = 8; public const URL_SCHEME_COLON_COORDINATES = 8;
/** public function URLSchemes($filter = null)
* @param self::URL_SCHEME_COLON_COORDINATES|self::URL_SCHEME_COLON_DOUBLE_SLASH|self::URL_SCHEME_NO_DOMAIN|self::URL_SCHEME_SINGLE_COLON $filter
*
* @return string[]
*/
public function URLSchemes(?int $filter = null): array
{ {
// TODO: move these to config // TODO: move these to config
$schemes = [ $schemes = [
@@ -204,7 +185,6 @@ class Link extends Component
* Intermediate callback for `replaceURLs()`, which helps resolve some * Intermediate callback for `replaceURLs()`, which helps resolve some
* ambiguous link forms before passing on to the final callback. * ambiguous link forms before passing on to the final callback.
* *
* @param string[] $matches
* @param callable(string $text): string $callback: return replacement text * @param callable(string $text): string $callback: return replacement text
*/ */
private function callbackHelper(array $matches, callable $callback): string private function callbackHelper(array $matches, callable $callback): string
@@ -284,7 +264,7 @@ class Link extends Component
return HTML::html(['a' => ['attrs' => $attrs, $url]], options: ['indent' => false]); return HTML::html(['a' => ['attrs' => $attrs, $url]], options: ['indent' => false]);
} }
public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult public function onNoteDeleteRelated(Note &$note, Actor $actor): bool
{ {
DB::wrapInTransaction(fn () => NoteToLink::removeWhereNoteId($note->getId())); DB::wrapInTransaction(fn () => NoteToLink::removeWhereNoteId($note->getId()));
return Event::next; return Event::next;

View File

@@ -35,7 +35,7 @@ declare(strict_types = 1);
namespace Component\Notification\Controller; namespace Component\Notification\Controller;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Util\Common; use App\Util\Common;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -44,8 +44,6 @@ class Feed extends Controller
{ {
/** /**
* Everything with attention to current user * Everything with attention to current user
*
* @return ControllerResultType
*/ */
public function notifications(Request $request): array public function notifications(Request $request): array
{ {
@@ -55,9 +53,9 @@ class Feed extends Controller
WHERE n.id IN ( WHERE n.id IN (
SELECT act.object_id FROM \App\Entity\Activity AS act SELECT act.object_id FROM \App\Entity\Activity AS act
WHERE act.object_type = 'note' AND act.id IN WHERE act.object_type = 'note' AND act.id IN
(SELECT att.activity_id FROM \Component\Notification\Entity\Notification AS att WHERE att.target_id = :target_id) (SELECT att.activity_id FROM \Component\Notification\Entity\Notification AS att WHERE att.target_id = :id)
) )
EOF, [':target_id' => $user->getId()]); EOF, ['id' => $user->getId()]);
return [ return [
'_template' => 'collection/notes.html.twig', '_template' => 'collection/notes.html.twig',
'page_title' => _m('Notifications'), 'page_title' => _m('Notifications'),

View File

@@ -24,51 +24,31 @@ namespace Component\Notification\Entity;
use App\Core\Entity; use App\Core\Entity;
/** /**
* Entity for object attentions * Entity for note attentions
*
* An attention is a form of persistent notification.
* It exists together and for as long as the object it belongs to.
* Creating an attention requires creating a Notification.
* *
* @category DB * @category DB
* @package GNUsocial * @package GNUsocial
* *
* @author Zach Copley <zach@status.net>
* @copyright 2010 StatusNet Inc.
* @author Mikael Nordfeldth <mmn@hethane.se>
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
* @author Diogo Peralta Cordeiro <@diogo.site> * @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2021-2022 Free Software Foundation, Inc http://www.fsf.org * @copyright 2022 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/ */
class Attention extends Entity class Attention extends Entity
{ {
// {{{ Autocode // {{{ Autocode
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
private string $object_type; private int $note_id;
private int $object_id;
private int $target_id; private int $target_id;
public function setObjectType(string $object_type): self public function setNoteId(int $note_id): self
{ {
$this->object_type = mb_substr($object_type, 0, 32); $this->note_id = $note_id;
return $this; return $this;
} }
public function getObjectType(): string public function getNoteId(): int
{ {
return $this->object_type; return $this->note_id;
}
public function setObjectId(int $object_id): self
{
$this->object_id = $object_id;
return $this;
}
public function getObjectId(): int
{
return $this->object_id;
} }
public function setTargetId(int $target_id): self public function setTargetId(int $target_id): self
@@ -88,16 +68,15 @@ class Attention extends Entity
public static function schemaDef(): array public static function schemaDef(): array
{ {
return [ return [
'name' => 'attention', 'name' => 'note_attention',
'description' => 'Attentions to actors (these are not mentions)', 'description' => 'Note attentions to actors (that are not a mention)',
'fields' => [ 'fields' => [
'object_type' => ['type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'the name of the table this object refers to'], 'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'note_id to give attention'],
'object_id' => ['type' => 'int', 'not null' => true, 'description' => 'id in the referenced table'], 'target_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'actor_id for feed receiver'],
'target_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'actor_id for feed receiver'],
], ],
'primary key' => ['object_type', 'object_id', 'target_id'], 'primary key' => ['note_id', 'target_id'],
'indexes' => [ 'indexes' => [
'attention_object_id_idx' => ['object_id'], 'attention_note_id_idx' => ['note_id'],
'attention_target_id_idx' => ['target_id'], 'attention_target_id_idx' => ['target_id'],
], ],
]; ];

View File

@@ -21,24 +21,24 @@ declare(strict_types = 1);
namespace Component\Notification\Entity; namespace Component\Notification\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use DateTimeInterface; use DateTimeInterface;
/** /**
* Entity for Notifications * Entity for attentions
*
* A Notification when isolated is a form of transient notification.
* When together with a persistent form of notification such as attentions or mentions,
* it records that the target was notified - which avoids re-notifying upon objects reconstructions.
* *
* @category DB * @category DB
* @package GNUsocial * @package GNUsocial
* *
* @author Diogo Peralta Cordeiro <@diogo.site> * @author Zach Copley <zach@status.net>
* @copyright 2021-2022 Free Software Foundation, Inc http://www.fsf.org * @copyright 2010 StatusNet Inc.
* @author Mikael Nordfeldth <mmn@hethane.se>
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
* @author Hugo Sales <hugo@hsal.es>
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/ */
class Notification extends Entity class Notification extends Entity
@@ -117,7 +117,7 @@ class Notification extends Entity
/** /**
* Pull the complete list of known activity context notifications for this activity. * Pull the complete list of known activity context notifications for this activity.
* *
* @return int[] actor ids (also group profiles) * @return array of integer actor ids (also group profiles)
*/ */
public static function getNotificationTargetIdsByActivity(int|Activity $activity_id): array public static function getNotificationTargetIdsByActivity(int|Activity $activity_id): array
{ {
@@ -129,17 +129,11 @@ class Notification extends Entity
return $targets; return $targets;
} }
/**
* @return int[]
*/
public function getNotificationTargetsByActivity(int|Activity $activity_id): array public function getNotificationTargetsByActivity(int|Activity $activity_id): array
{ {
return DB::findBy(Actor::class, ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]); return DB::findBy(Actor::class, ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]);
} }
/**
* @return int[]
*/
public static function getAllActivitiesTargetedAtActor(Actor $actor): array public static function getAllActivitiesTargetedAtActor(Actor $actor): array
{ {
return DB::dql(<<<'EOF' return DB::dql(<<<'EOF'

View File

@@ -21,26 +21,26 @@ declare(strict_types = 1);
namespace Component\Notification; namespace Component\Notification;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Queue; use App\Core\Queue\Queue;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use Component\FreeNetwork\FreeNetwork; use Component\FreeNetwork\FreeNetwork;
use Component\Notification\Controller\Feed; use Component\Notification\Controller\Feed;
use EventResult;
use Exception; use Exception;
use Throwable; use Throwable;
class Notification extends Component class Notification extends Component
{ {
public function onAddRoute(Router $m): EventResult public function onAddRoute(RouteLoader $m): bool
{ {
$m->connect('feed_notifications', '/feed/notifications', [Feed::class, 'notifications']); $m->connect('feed_notifications', '/feed/notifications', [Feed::class, 'notifications']);
return Event::next; return Event::next;
@@ -49,7 +49,7 @@ class Notification extends Component
/** /**
* @throws ServerException * @throws ServerException
*/ */
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): bool
{ {
DB::persist(\App\Entity\Feed::create([ DB::persist(\App\Entity\Feed::create([
'actor_id' => $actor_id, 'actor_id' => $actor_id,
@@ -64,59 +64,24 @@ class Notification extends Component
/** /**
* Enqueues a notification for an Actor (such as person or group) which means * Enqueues a notification for an Actor (such as person or group) which means
* it shows up in their home feed and such. * it shows up in their home feed and such.
* WARNING: It's highly advisable to have flushed any relevant objects before triggering this event.
*
* $targets should be of the shape:
* (int|Actor)[] // Prefer Actor whenever possible
* Example of $targets:
* [42, $actor_alice, $actor_bob] // Avoid repeating actors or ids
*
* @param Actor $sender The one responsible for this activity, take care not to include it in targets
* @param Activity $activity The activity responsible for the object being given to known to targets
* @param non-empty-array<Actor|int> $targets Attentions, Mentions, any other source. Should never be empty, you usually want to register an attention to every $sender->getSubscribers()
* @param null|string $reason An optional reason explaining why this notification exists
*/ */
public function onNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): EventResult public function onNewNotification(Actor $sender, Activity $activity, array $ids_already_known = [], ?string $reason = null): bool
{ {
// Ensure targets are all actor objects and unique $targets = $activity->getNotificationTargets(ids_already_known: $ids_already_known, sender_id: $sender->getId());
$effective_targets = []; if (Event::handle('NewNotificationWithTargets', [$sender, $activity, $targets, $reason]) === Event::next) {
foreach ($targets as $target) { self::notify($sender, $activity, $targets, $reason);
if (\is_int($target)) {
$target_id = $target;
$target_object = null;
} else {
$target_id = $target->getId();
$target_object = $target;
}
if (!\array_key_exists(key: $target_id, array: $effective_targets)) {
$target_object ??= Actor::getById($target_id);
$effective_targets[$target_id] = $target_object;
}
}
unset($targets);
if (Event::handle('NewNotificationStart', [$sender, $activity, $effective_targets, $reason]) === Event::next) {
self::notify($sender, $activity, $effective_targets, $reason);
} }
Event::handle('NewNotificationEnd', [$sender, $activity, $effective_targets, $reason]);
return Event::next; return Event::next;
} }
/** public function onQueueNotificationLocal(Actor $sender, Activity $activity, Actor $target, ?string $reason, array &$retry_args): bool
* @param mixed[] $retry_args
*/
public function onQueueNotificationLocal(Actor $sender, Activity $activity, Actor $target, ?string $reason, array &$retry_args): EventResult
{ {
// TODO: use https://symfony.com/doc/current/notifier.html // TODO: use https://symfony.com/doc/current/notifier.html
return Event::stop; return Event::stop;
} }
/** public function onQueueNotificationRemote(Actor $sender, Activity $activity, array $targets, ?string $reason, array &$retry_args): bool
* @param Actor[] $targets
* @param mixed[] $retry_args
*/
public function onQueueNotificationRemote(Actor $sender, Activity $activity, array $targets, ?string $reason, array &$retry_args): EventResult
{ {
if (FreeNetwork::notify($sender, $activity, $targets, $reason)) { if (FreeNetwork::notify($sender, $activity, $targets, $reason)) {
return Event::stop; return Event::stop;
@@ -126,24 +91,21 @@ class Notification extends Component
} }
/** /**
* Bring given Activity to Targets' knowledge. * Bring given Activity to Targets's attention
* This will flush a Notification to DB.
* *
* @param Actor[] $targets * @return true if successful, false otherwise
*
* @return bool true if successful, false otherwise
*/ */
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
{ {
$remote_targets = []; $remote_targets = [];
foreach ($targets as $target) { foreach ($targets as $target) {
if ($target->getIsLocal()) { if ($target->getIsLocal()) {
if ($target->hasBlocked($author = $activity->getActor())) { if ($target->hasBlocked($activity->getActor())) {
Log::info("Not saving notification to actor {$target->getId()} from sender {$sender->getId()} because receiver blocked author {$author->getId()}."); Log::info("Not saving reply to actor {$target->getId()} from sender {$sender->getId()} because of a block.");
continue; continue;
} }
if (Event::handle('NewNotificationShould', [$activity, $target]) === Event::next) { if (Event::handle('NewNotificationShould', [$activity, $target]) === Event::next) {
if ($sender->getId() === $target->getId() if ($sender->getId() === $target->getId()
|| $activity->getActorId() === $target->getId()) { || $activity->getActorId() === $target->getId()) {
// The target already knows about this, no need to bother with a notification // The target already knows about this, no need to bother with a notification
continue; continue;
@@ -151,7 +113,7 @@ class Notification extends Component
} }
Queue::enqueue( Queue::enqueue(
payload: [$sender, $activity, $target, $reason], payload: [$sender, $activity, $target, $reason],
queue: 'NotificationLocal', queue: 'notification_local',
priority: true, priority: true,
); );
} else { } else {
@@ -162,7 +124,7 @@ class Notification extends Component
} }
// XXX: Unideal as in failures the rollback will leave behind a false notification, // XXX: Unideal as in failures the rollback will leave behind a false notification,
// but most notifications (all) require flushing the objects first // but most notifications (all) require flushing the objects first
// Should be okay as long as implementations bear this in mind // Should be okay as long as implementors bear this in mind
try { try {
DB::wrapInTransaction(fn () => DB::persist(Entity\Notification::create([ DB::wrapInTransaction(fn () => DB::persist(Entity\Notification::create([
'activity_id' => $activity->getId(), 'activity_id' => $activity->getId(),
@@ -170,7 +132,7 @@ class Notification extends Component
'reason' => $reason, 'reason' => $reason,
]))); ])));
} catch (Exception|Throwable $e) { } catch (Exception|Throwable $e) {
// We do our best not to record duplicate notifications, but it's not insane that can happen // We do our best not to record duplicated notifications, but it's not insane that can happen
Log::error('It was attempted to record an invalid notification!', [$e]); Log::error('It was attempted to record an invalid notification!', [$e]);
} }
} }
@@ -178,7 +140,7 @@ class Notification extends Component
if ($remote_targets !== []) { if ($remote_targets !== []) {
Queue::enqueue( Queue::enqueue(
payload: [$sender, $activity, $remote_targets, $reason], payload: [$sender, $activity, $remote_targets, $reason],
queue: 'NotificationRemote', queue: 'notification_remote',
priority: false, priority: false,
); );
} }

View File

@@ -24,7 +24,7 @@ declare(strict_types = 1);
namespace Component\Person\Controller; namespace Component\Person\Controller;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity as E; use App\Entity as E;
use App\Entity\LocalUser; use App\Entity\LocalUser;
@@ -34,16 +34,11 @@ use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController; use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class PersonFeed extends FeedController class PersonFeed extends FeedController
{ {
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function personViewId(Request $request, int $id): array public function personViewId(Request $request, int $id): array
{ {
@@ -67,8 +62,6 @@ class PersonFeed extends FeedController
* *
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function personViewNickname(Request $request, string $nickname): array public function personViewNickname(Request $request, string $nickname): array
{ {
@@ -80,9 +73,6 @@ class PersonFeed extends FeedController
return $this->personView($request, $person); return $this->personView($request, $person);
} }
/**
* @return ControllerResultType
*/
public function personView(Request $request, Actor $person): array public function personView(Request $request, Actor $person): array
{ {
return [ return [

View File

@@ -38,12 +38,11 @@ namespace Component\Person\Controller;
// {{{ Imports // {{{ Imports
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\AuthenticationException; use App\Util\Exception\AuthenticationException;
use App\Util\Exception\NicknameEmptyException; use App\Util\Exception\NicknameEmptyException;
@@ -88,15 +87,12 @@ class PersonSettings extends Controller
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function allSettings(Request $request, LanguageController $language): array public function allSettings(Request $request, LanguageController $language): array
{ {
// Ensure the user is logged in and retrieve Actor object for given user // Ensure the user is logged in and retrieve Actor object for given user
$user = Common::ensureLoggedIn(); $user = Common::ensureLoggedIn();
// Must be persisted $actor = $user->getActor();
$actor = DB::findOneBy(Actor::class, ['id' => $user->getId()]);
$personal_form = ActorForms::personalInfo(request: $request, scope: $actor, target: $actor); $personal_form = ActorForms::personalInfo(request: $request, scope: $actor, target: $actor);
$email_form = self::email($request); $email_form = self::email($request);
@@ -105,7 +101,7 @@ class PersonSettings extends Controller
$language_form = $language->settings($request); $language_form = $language->settings($request);
return [ return [
'_template' => 'person/settings.html.twig', '_template' => 'settings/base.html.twig',
'personal_info_form' => $personal_form->createView(), 'personal_info_form' => $personal_form->createView(),
'email_form' => $email_form->createView(), 'email_form' => $email_form->createView(),
'password_form' => $password_form->createView(), 'password_form' => $password_form->createView(),
@@ -207,8 +203,6 @@ class PersonSettings extends Controller
* @throws \Doctrine\DBAL\Exception * @throws \Doctrine\DBAL\Exception
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType[]
*/ */
private static function notifications(Request $request): array private static function notifications(Request $request): array
{ {
@@ -255,7 +249,7 @@ class PersonSettings extends Controller
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
Log::critical("Structure of table user_notification_prefs changed in a way not accounted to in notification settings ({$name}): " . $type_str); Log::critical("Structure of table user_notification_prefs changed in a way not accounted to in notification settings ({$name}): " . $type_str);
throw new ServerException(_m('Internal server error')); throw new ServerException(_m('Internal server error'));
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
} }
@@ -290,7 +284,7 @@ class PersonSettings extends Controller
$data = $form->getData(); $data = $form->getData();
unset($data['translation_domain']); unset($data['translation_domain']);
try { try {
[$entity, $is_update] = UserNotificationPrefs::checkExistingAndCreateOrUpdate( [$entity, $is_update] = UserNotificationPrefs::createOrUpdate(
array_merge(['user_id' => $user->getId(), 'transport' => $transport_name], $data), array_merge(['user_id' => $user->getId(), 'transport' => $transport_name], $data),
find_by_keys: ['user_id', 'transport'], find_by_keys: ['user_id', 'transport'],
); );

View File

@@ -23,14 +23,13 @@ namespace Component\Person;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Util\Nickname; use App\Util\Nickname;
use Component\Person\Controller as C; use Component\Person\Controller as C;
use EventResult;
class Person extends Component class Person extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect(id: 'person_actor_view_id', uri_path: '/person/{id<\d+>}', target: [C\PersonFeed::class, 'personViewId']); $r->connect(id: 'person_actor_view_id', uri_path: '/person/{id<\d+>}', target: [C\PersonFeed::class, 'personViewId']);
$r->connect(id: 'person_actor_view_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\PersonFeed::class, 'personViewNickname'], options: ['is_system_path' => false]); $r->connect(id: 'person_actor_view_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\PersonFeed::class, 'personViewNickname'], options: ['is_system_path' => false]);

View File

@@ -23,8 +23,8 @@ declare(strict_types = 1);
namespace Component\Person\tests\Controller; namespace Component\Person\tests\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Util\GNUsocialTestCase; use App\Util\GNUsocialTestCase;
use Jchook\AssertThrows\AssertThrows; use Jchook\AssertThrows\AssertThrows;
@@ -33,6 +33,10 @@ class PersonSettingsTest extends GNUsocialTestCase
{ {
use AssertThrows; use AssertThrows;
/**
* @covers \App\Controller\PersonSettings::allSettings
* @covers \App\Controller\PersonSettings::personalInfo
*/
public function testPersonalInfo() public function testPersonalInfo()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -51,15 +55,19 @@ class PersonSettingsTest extends GNUsocialTestCase
]); ]);
$changed_user = DB::findOneBy(LocalUser::class, ['id' => $user->getId()]); $changed_user = DB::findOneBy(LocalUser::class, ['id' => $user->getId()]);
$actor = $changed_user->getActor(); $actor = $changed_user->getActor();
static::assertSame('form_test_user_new_nickname', $changed_user->getNickname()); static::assertSame($changed_user->getNickname(), 'form_test_user_new_nickname');
static::assertSame('form_test_user_new_nickname', $actor->getNickname()); static::assertSame($actor->getNickname(), 'form_test_user_new_nickname');
static::assertSame('Form User', $actor->getFullName()); static::assertSame($actor->getFullName(), 'Form User');
static::assertSame('https://gnu.org', $actor->getHomepage()); static::assertSame($actor->getHomepage(), 'https://gnu.org');
static::assertSame('I was born at a very young age', $actor->getBio()); static::assertSame($actor->getBio(), 'I was born at a very young age');
static::assertSame('right here', $actor->getLocation()); static::assertSame($actor->getLocation(), 'right here');
// static::assertSame('908555842', $changed_user->getPhoneNumber()->getNationalNumber()); // static::assertSame($changed_user->getPhoneNumber()->getNationalNumber(), '908555842');
} }
/**
* @covers \App\Controller\PersonSettings::account
* @covers \App\Controller\PersonSettings::allSettings
*/
public function testEmail() public function testEmail()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -78,6 +86,10 @@ class PersonSettingsTest extends GNUsocialTestCase
static::assertSame($changed_user->getIncomingEmail(), 'incoming@provider.any'); static::assertSame($changed_user->getIncomingEmail(), 'incoming@provider.any');
} }
/**
* @covers \App\Controller\PersonSettings::account
* @covers \App\Controller\PersonSettings::allSettings
*/
public function testCorrectPassword() public function testCorrectPassword()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -96,6 +108,10 @@ class PersonSettingsTest extends GNUsocialTestCase
static::assertTrue($changed_user->checkPassword('this is some test password')); static::assertTrue($changed_user->checkPassword('this is some test password'));
} }
/**
* @covers \App\Controller\PersonSettings::account
* @covers \App\Controller\PersonSettings::allSettings
*/
public function testAccountWrongPassword() public function testAccountWrongPassword()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -113,7 +129,11 @@ class PersonSettingsTest extends GNUsocialTestCase
$this->assertSelectorTextContains('.stacktrace', 'AuthenticationException'); $this->assertSelectorTextContains('.stacktrace', 'AuthenticationException');
} }
// TODO: First actually implement this functionality // TODO: First actually implement this functionality
// /**
// * @covers \App\Controller\PersonSettings::allSettings
// * @covers \App\Controller\PersonSettings::notifications
// */
// public function testNotifications() // public function testNotifications()
// { // {
// $client = static::createClient(); // $client = static::createClient();

View File

@@ -52,7 +52,7 @@ class Posting extends Controller
content_type: $data['content_type'], content_type: $data['content_type'],
locale: $data['language'], locale: $data['language'],
scope: VisibilityScope::from($data['visibility']), scope: VisibilityScope::from($data['visibility']),
attentions: isset($target) ? [$target] : [], targets: isset($target) ? [$target] : [],
reply_to: \array_key_exists('reply_to_id', $data) ? $data['reply_to_id'] : null, reply_to: \array_key_exists('reply_to_id', $data) ? $data['reply_to_id'] : null,
attachments: $data['attachments'], attachments: $data['attachments'],
process_note_content_extra_args: $extra_args, process_note_content_extra_args: $extra_args,
@@ -61,9 +61,9 @@ class Posting extends Controller
return Core\Form::forceRedirect($form, $request); return Core\Form::forceRedirect($form, $request);
} }
} catch (FormSizeFileException $e) { } catch (FormSizeFileException $e) {
throw new ClientException(_m('Invalid file size given.'), previous: $e); throw new ClientException(_m('Invalid file size given'), previous: $e);
} }
} }
throw new ClientException(_m('Invalid form submission.')); throw new ClientException(_m('Invalid form submission'));
} }
} }

View File

@@ -8,7 +8,7 @@ use App\Core\ActorLocalRoles;
use App\Core\Event; use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Router; use App\Core\Router\Router;
use App\Core\VisibilityScope; use App\Core\VisibilityScope;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;

View File

@@ -24,16 +24,16 @@ declare(strict_types = 1);
namespace Component\Posting; namespace Component\Posting;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\GSFile; use App\Core\GSFile;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Core\VisibilityScope; use App\Core\VisibilityScope;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\BugFoundException; use App\Util\Exception\BugFoundException;
@@ -44,13 +44,11 @@ use App\Util\Exception\ServerException;
use App\Util\Formatting; use App\Util\Formatting;
use App\Util\HTML; use App\Util\HTML;
use Component\Attachment\Entity\ActorToAttachment; use Component\Attachment\Entity\ActorToAttachment;
use Component\Attachment\Entity\Attachment;
use Component\Attachment\Entity\AttachmentToNote; use Component\Attachment\Entity\AttachmentToNote;
use Component\Conversation\Conversation; use Component\Conversation\Conversation;
use Component\Language\Entity\Language; use Component\Language\Entity\Language;
use Component\Notification\Entity\Attention; use Component\Notification\Entity\Attention;
use EventResult; use Functional as F;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -58,7 +56,7 @@ class Posting extends Component
{ {
public const route = 'posting_form_action'; public const route = 'posting_form_action';
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect(self::route, '/form/posting', Controller\Posting::class); $r->connect(self::route, '/form/posting', Controller\Posting::class);
return Event::next; return Event::next;
@@ -68,15 +66,13 @@ class Posting extends Component
* HTML render event handler responsible for adding and handling * HTML render event handler responsible for adding and handling
* the result of adding the note submission form, only if a user is logged in * the result of adding the note submission form, only if a user is logged in
* *
* @param array{post_form?: FormInterface} $res
*
* @throws BugFoundException * @throws BugFoundException
* @throws ClientException * @throws ClientException
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*/ */
public function onAddMainRightPanelBlock(Request $request, array &$res): EventResult public function onAddMainRightPanelBlock(Request $request, array &$res): bool
{ {
if (\is_null($user = Common::user()) || preg_match('(feed|conversation|group|view)', $request->get('_route')) === 0) { if (\is_null($user = Common::user()) || preg_match('(feed|conversation|group|view)', $request->get('_route')) === 0) {
return Event::next; return Event::next;
@@ -88,33 +84,17 @@ class Posting extends Component
} }
/** /**
* @param Actor $actor The Actor responsible for the creation of this Note
* @param null|string $content The raw text content
* @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
* @param null|string $locale Note's written text language, set by the default Actor language or upon filling
* @param null|VisibilityScope $scope The visibility of this Note
* @param Actor[]|int[] $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and target
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
* @param UploadedFile[] $attachments UploadedFile[] to be stored as GSFiles associated to this note
* @param array<array{Attachment, string}> $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
* @param array{note?: Note, content?: string, content_type?: string, extra_args?: array<string, mixed>} $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
* @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
* @param string $source The source of this Note
*
* @throws ClientException * @throws ClientException
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws ServerException * @throws ServerException
*
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
*/ */
public static function storeLocalArticle( public static function storeLocalPage(
Actor $actor, Actor $actor,
?string $content, ?string $content,
string $content_type, string $content_type,
?string $locale = null, ?string $locale = null,
?VisibilityScope $scope = null, ?VisibilityScope $scope = null,
array $attentions = [], array $targets = [],
null|int|Note $reply_to = null, null|int|Note $reply_to = null,
array $attachments = [], array $attachments = [],
array $processed_attachments = [], array $processed_attachments = [],
@@ -124,13 +104,13 @@ class Posting extends Component
string $source = 'web', string $source = 'web',
?string $title = null, ?string $title = null,
): array { ): array {
[$activity, $note, $effective_attentions] = self::storeLocalNote( [$activity, $note, $attention_ids] = self::storeLocalNote(
actor: $actor, actor: $actor,
content: $content, content: $content,
content_type: $content_type, content_type: $content_type,
locale: $locale, locale: $locale,
scope: $scope, scope: $scope,
attentions: $attentions, targets: $targets,
reply_to: $reply_to, reply_to: $reply_to,
attachments: $attachments, attachments: $attachments,
processed_attachments: $processed_attachments, processed_attachments: $processed_attachments,
@@ -139,24 +119,16 @@ class Posting extends Component
rendered: $rendered, rendered: $rendered,
source: $source, source: $source,
); );
$note->setType('article'); $note->setType('page');
$note->setTitle($title); $note->setTitle($title);
if ($flush_and_notify) { if ($flush_and_notify) {
// Flush before notification // Flush before notification
DB::flush(); DB::flush();
Event::handle('NewNotification', [ Event::handle('NewNotification', [$actor, $activity, ['object' => $attention_ids], _m('{nickname} created a page {note_id}.', ['{nickname}' => $actor->getNickname(), '{note_id}' => $activity->getObjectId()])]);
$actor,
$activity,
$effective_attentions,
_m('Actor {actor_id} created article {note_id}.', [
'{actor_id}' => $actor->getId(),
'{note_id}' => $activity->getObjectId(),
]),
]);
} }
return [$activity, $note, $effective_attentions]; return [$activity, $note, $attention_ids];
} }
/** /**
@@ -164,25 +136,25 @@ class Posting extends Component
* $actor_id, possibly as a reply to note $reply_to and with flag * $actor_id, possibly as a reply to note $reply_to and with flag
* $is_local. Sanitizes $content and $attachments * $is_local. Sanitizes $content and $attachments
* *
* @param Actor $actor The Actor responsible for the creation of this Note * @param Actor $actor The Actor responsible for the creation of this Note
* @param null|string $content The raw text content * @param null|string $content The raw text content
* @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...) * @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
* @param null|string $locale Note's written text language, set by the default Actor language or upon filling * @param null|string $locale Note's written text language, set by the default Actor language or upon filling
* @param null|VisibilityScope $scope The visibility of this Note * @param null|VisibilityScope $scope The visibility of this Note
* @param Actor[]|int[] $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and targte * @param array $targets Actor|int[]: In Group/To Person or Bot, registers an attention between note and target
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself * @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
* @param UploadedFile[] $attachments UploadedFile[] to be stored as GSFiles associated to this note * @param array $attachments UploadedFile[] to be stored as GSFiles associated to this note
* @param array<array{Attachment, string}> $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note * @param array $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
* @param array{note?: Note, content?: string, content_type?: string, extra_args?: array<string, mixed>} $process_note_content_extra_args Extra arguments for the event ProcessNoteContent * @param array $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification * @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
* @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent * @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
* @param string $source The source of this Note * @param string $source The source of this Note
* *
* @throws ClientException * @throws ClientException
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws ServerException * @throws ServerException
* *
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>} * @return array [Activity, Note, int[]] Activity, Note, Attention Ids
*/ */
public static function storeLocalNote( public static function storeLocalNote(
Actor $actor, Actor $actor,
@@ -190,7 +162,7 @@ class Posting extends Component
string $content_type, string $content_type,
?string $locale = null, ?string $locale = null,
?VisibilityScope $scope = null, ?VisibilityScope $scope = null,
array $attentions = [], array $targets = [],
null|int|Note $reply_to = null, null|int|Note $reply_to = null,
array $attachments = [], array $attachments = [],
array $processed_attachments = [], array $processed_attachments = [],
@@ -201,9 +173,7 @@ class Posting extends Component
): array { ): array {
$scope ??= VisibilityScope::EVERYWHERE; // TODO: If site is private, default to LOCAL $scope ??= VisibilityScope::EVERYWHERE; // TODO: If site is private, default to LOCAL
$reply_to_id = \is_null($reply_to) ? null : (\is_int($reply_to) ? $reply_to : $reply_to->getId()); $reply_to_id = \is_null($reply_to) ? null : (\is_int($reply_to) ? $reply_to : $reply_to->getId());
$mentions = [];
/** @var array<int, array{ mentioned?: array<int, Actor|LocalUser> }> $mentions */
$mentions = [];
if (\is_null($rendered) && !empty($content)) { if (\is_null($rendered) && !empty($content)) {
Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $locale, &$mentions]); Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $locale, &$mentions]);
} }
@@ -239,7 +209,7 @@ class Posting extends Component
if (!\is_null($reply_to_id)) { if (!\is_null($reply_to_id)) {
Cache::incr(Note::cacheKeys($reply_to_id)['replies-count']); Cache::incr(Note::cacheKeys($reply_to_id)['replies-count']);
// Not having them cached doesn't mean replies don't exist, but don't push it to the // Not having them cached doesn't mean replies don't exist, but don't push it to the
// list, as that means they need to be re-fetched, or some would be missed // list, as that means they need to be refetched, or some would be missed
if (Cache::exists(Note::cacheKeys($reply_to_id)['replies'])) { if (Cache::exists(Note::cacheKeys($reply_to_id)['replies'])) {
Cache::listPushRight(Note::cacheKeys($reply_to_id)['replies'], $note); Cache::listPushRight(Note::cacheKeys($reply_to_id)['replies'], $note);
} }
@@ -251,12 +221,12 @@ class Posting extends Component
Event::handle('ProcessNoteContent', [$note, $content, $content_type, $process_note_content_extra_args]); Event::handle('ProcessNoteContent', [$note, $content, $content_type, $process_note_content_extra_args]);
} }
// These are note attachments now, and not just attachments, ensure these relations are respected // These are note attachments now, and not just attachments, ensure these relations are ensured
if ($processed_attachments !== []) { if ($processed_attachments !== []) {
foreach ($processed_attachments as [$a, $fname]) { foreach ($processed_attachments as [$a, $fname]) {
// Most attachments should already be associated with its author, but maybe it didn't make sense // Most attachments should already be associated with its author, but maybe it didn't make sense
//for this attachment, or it's simply a repost of an attachment by a different actor //for this attachment, or it's simply a repost of an attachment by a different actor
if (DB::count(ActorToAttachment::class, $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor->getId()]) === 0) { if (DB::count('actor_to_attachment', $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor->getId()]) === 0) {
DB::persist(ActorToAttachment::create($args)); DB::persist(ActorToAttachment::create($args));
} }
DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $note->getId(), 'title' => $fname])); DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $note->getId(), 'title' => $fname]));
@@ -272,38 +242,13 @@ class Posting extends Component
]); ]);
DB::persist($activity); DB::persist($activity);
$effective_attentions = []; $attention_ids = [];
foreach ($attentions as $target) { foreach ($targets as $target) {
if (\is_int($target)) { $target_id = \is_int($target) ? $target : $target->getId();
$target_id = $target; DB::persist(Attention::create(['note_id' => $note->getId(), 'target_id' => $target_id]));
$add = !\array_key_exists($target_id, $effective_attentions); $attention_ids[$target_id] = true;
$effective_attentions[$target_id] = $target;
} else {
$target_id = $target->getId();
if ($add = !\array_key_exists($target_id, $effective_attentions)) {
$effective_attentions[$target_id] = $target_id;
}
}
if ($add) {
DB::persist(Attention::create(['object_type' => Note::schemaName(), 'object_id' => $note->getId(), 'target_id' => $target_id]));
}
}
foreach ($mentions as $m) {
foreach ($m['mentioned'] ?? [] as $mentioned) {
$target_id = $mentioned->getId();
if (!\array_key_exists($target_id, $effective_attentions)) {
DB::persist(Attention::create(['object_type' => Note::schemaName(), 'object_id' => $note->getId(), 'target_id' => $target_id]));
}
$effective_attentions[$target_id] = $mentioned;
}
}
foreach ($actor->getSubscribers() as $subscriber) {
$target_id = $subscriber->getId();
DB::persist(Attention::create(['object_type' => Activity::schemaName(), 'object_id' => $activity->getId(), 'target_id' => $target_id]));
$effective_attentions[$target_id] = $subscriber;
} }
$attention_ids = array_keys($attention_ids);
if ($flush_and_notify) { if ($flush_and_notify) {
// Flush before notification // Flush before notification
@@ -311,21 +256,21 @@ class Posting extends Component
Event::handle('NewNotification', [ Event::handle('NewNotification', [
$actor, $actor,
$activity, $activity,
$effective_attentions, [
_m('Actor {actor_id} created note {note_id}.', [ 'note-attention' => $attention_ids,
'{actor_id}' => $actor->getId(), 'object' => F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId()))),
],
_m('{nickname} created a note {note_id}.', [
'{nickname}' => $actor->getNickname(),
'{note_id}' => $activity->getObjectId(), '{note_id}' => $activity->getObjectId(),
]), ]),
]); ]);
} }
return [$activity, $note, $effective_attentions]; return [$activity, $note, $attention_ids];
} }
/** public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = [])
* @param array<int, \App\Entity\Actor> $mentions
*/
public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = []): EventResult
{ {
switch ($content_type) { switch ($content_type) {
case 'text/plain': case 'text/plain':

View File

@@ -38,17 +38,12 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Search extends FeedController class Search extends FeedController
{ {
/** /**
* Handle a search query * Handle a search query
*
* @return ControllerResultType
*/ */
public function handle(Request $request): array public function handle(Request $request)
{ {
$actor = Common::actor(); $actor = Common::actor();
$language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null; $language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null;

View File

@@ -27,12 +27,10 @@ use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use App\Util\Formatting; use App\Util\Formatting;
use EventResult;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
@@ -41,10 +39,9 @@ use Symfony\Component\HttpFoundation\Request;
class Search extends Component class Search extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute($r)
{ {
$r->connect('search', '/search', Controller\Search::class); $r->connect('search', '/search', Controller\Search::class);
return EventResult::next;
} }
/** /**
@@ -134,11 +131,9 @@ class Search extends Component
/** /**
* Add the search form to the site header * Add the search form to the site header
* *
* @param string[] $elements
*
* @throws RedirectException * @throws RedirectException
*/ */
public function onPrependRightPanelBlock(Request $request, array &$elements): EventResult public function onPrependRightPanelBlock(Request $request, array &$elements): bool
{ {
$elements[] = Formatting::twigRenderFile('cards/search/view.html.twig', ['search' => self::searchForm($request)]); $elements[] = Formatting::twigRenderFile('cards/search/view.html.twig', ['search' => self::searchForm($request)]);
return Event::next; return Event::next;
@@ -147,9 +142,11 @@ class Search extends Component
/** /**
* Output our dedicated stylesheet * Output our dedicated stylesheet
* *
* @param string[] $styles stylesheets path * @param array $styles stylesheets path
*
* @return bool hook value; true means continue processing, false means stop
*/ */
public function onEndShowStyles(array &$styles, string $route): EventResult public function onEndShowStyles(array &$styles, string $route): bool
{ {
$styles[] = 'components/Search/assets/css/view.css'; $styles[] = 'components/Search/assets/css/view.css';
return Event::next; return Event::next;

View File

@@ -23,11 +23,11 @@ declare(strict_types = 1);
namespace Component\Subscription\Controller; namespace Component\Subscription\Controller;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
@@ -45,8 +45,6 @@ class Subscribers extends CircleController
{ {
/** /**
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function subscribersByActor(Request $request, Actor $actor): array public function subscribersByActor(Request $request, Actor $actor): array
{ {
@@ -63,8 +61,6 @@ class Subscribers extends CircleController
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function subscribersByActorId(Request $request, int $id): array public function subscribersByActorId(Request $request, int $id): array
{ {
@@ -82,8 +78,6 @@ class Subscribers extends CircleController
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
* @throws RedirectException * @throws RedirectException
*
* @return ControllerResultType
*/ */
public function subscribersAdd(Request $request, int $object_id): array public function subscribersAdd(Request $request, int $object_id): array
{ {
@@ -132,8 +126,6 @@ class Subscribers extends CircleController
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
* @throws RedirectException * @throws RedirectException
*
* @return ControllerResultType
*/ */
public function subscribersRemove(Request $request, int $object_id): array public function subscribersRemove(Request $request, int $object_id): array
{ {

View File

@@ -38,8 +38,6 @@ class Subscriptions extends CircleController
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function subscriptionsByActorId(Request $request, int $id): array public function subscriptionsByActorId(Request $request, int $id): array
{ {
@@ -50,10 +48,7 @@ class Subscriptions extends CircleController
return $this->subscriptionsByActor($request, $actor); return $this->subscriptionsByActor($request, $actor);
} }
/** public function subscriptionsByActor(Request $request, Actor $actor)
* @return ControllerResultType
*/
public function subscriptionsByActor(Request $request, Actor $actor): array
{ {
return [ return [
'_template' => 'collection/actors.html.twig', '_template' => 'collection/actors.html.twig',

View File

@@ -114,6 +114,27 @@ class ActorSubscription extends Entity
]; ];
} }
/**
* @see Entity->getNotificationTargetIds
*/
public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array
{
if (!\array_key_exists('object', $ids_already_known)) {
$target_ids = [$this->getSubscribedId()]; // The object of any subscription is the one subscribed (or unsubscribed)
} else {
$target_ids = $ids_already_known['object'];
}
// Additional actors that should know about this
if ($include_additional && \array_key_exists('additional', $ids_already_known)) {
array_push($target_ids, ...$ids_already_known['additional']);
} else {
return $target_ids;
}
return array_unique($target_ids);
}
public static function schemaDef(): array public static function schemaDef(): array
{ {
return [ return [

View File

@@ -24,11 +24,12 @@ declare(strict_types = 1);
namespace Component\Subscription; namespace Component\Subscription;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser; use App\Entity\LocalUser;
@@ -36,15 +37,14 @@ use App\Util\Common;
use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\DuplicateFoundException;
use App\Util\Exception\NotFoundException; use App\Util\Exception\NotFoundException;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use Component\Notification\Entity\Attention;
use Component\Subscription\Controller\Subscribers as SubscribersController; use Component\Subscription\Controller\Subscribers as SubscribersController;
use Component\Subscription\Controller\Subscriptions as SubscriptionsController; use Component\Subscription\Controller\Subscriptions as SubscriptionsController;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Subscription extends Component class Subscription extends Component
{ {
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect(id: 'actor_subscribe_add', uri_path: '/actor/subscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersAdd']); $r->connect(id: 'actor_subscribe_add', uri_path: '/actor/subscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersAdd']);
$r->connect(id: 'actor_subscribe_remove', uri_path: '/actor/unsubscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersRemove']); $r->connect(id: 'actor_subscribe_remove', uri_path: '/actor/unsubscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersRemove']);
@@ -58,8 +58,6 @@ class Subscription extends Component
* *
* @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed * @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed
* @param Actor|int|LocalUser $object The Actor who was subscribed or unsubscribed from * @param Actor|int|LocalUser $object The Actor who was subscribed or unsubscribed from
*
* @return array{bool, bool}
*/ */
public static function refreshSubscriptionCount(int|Actor|LocalUser $subject, int|Actor|LocalUser $object): array public static function refreshSubscriptionCount(int|Actor|LocalUser $subject, int|Actor|LocalUser $object): array
{ {
@@ -99,24 +97,22 @@ class Subscription extends Component
$subscription = DB::findOneBy(table: Entity\ActorSubscription::class, criteria: $opts, return_null: true); $subscription = DB::findOneBy(table: Entity\ActorSubscription::class, criteria: $opts, return_null: true);
$activity = null; $activity = null;
if (\is_null($subscription)) { if (\is_null($subscription)) {
DB::persist($subscription = Entity\ActorSubscription::create($opts)); DB::persist(Entity\ActorSubscription::create($opts));
$activity = Activity::create([ $activity = Activity::create([
'actor_id' => $subscriber_id, 'actor_id' => $subscriber_id,
'verb' => 'subscribe', 'verb' => 'subscribe',
'object_type' => Actor::schemaName(), 'object_type' => 'actor',
'object_id' => $subscribed_id, 'object_id' => $subscribed_id,
'source' => $source, 'source' => $source,
]); ]);
DB::persist($activity); DB::persist($activity);
DB::persist(Attention::create(['object_type' => Activity::schemaName(), 'object_id' => $activity->getId(), 'target_id' => $subscribed_id]));
Event::handle('NewNotification', [ Event::handle('NewNotification', [
\is_int($subject) ? $subject : Actor::getById($subscriber_id), \is_int($subject) ? $subject : Actor::getById($subscriber_id),
$activity, $activity,
[$subscribed_id], ['object' => [$activity->getObjectId()]],
$reason = _m('{subject} subscribed to {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $activity->getObjectId()]), _m('{subject} subscribed to {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $activity->getObjectId()]),
]); ]);
Event::handle('NewSubscriptionEnd', [$subject, $activity, $object, $reason]);
} }
return $activity; return $activity;
} }
@@ -150,22 +146,21 @@ class Subscription extends Component
if (!\is_null($subscription)) { if (!\is_null($subscription)) {
// Remove Subscription // Remove Subscription
DB::remove($subscription); DB::remove($subscription);
$previous_follow_activity = DB::findBy(Activity::class, ['verb' => 'subscribe', 'object_type' => Actor::schemaName(), 'object_id' => $subscribed_id], order_by: ['created' => 'DESC'])[0]; $previous_follow_activity = DB::findBy('activity', ['verb' => 'subscribe', 'object_type' => 'actor', 'object_id' => $subscribed_id], order_by: ['created' => 'DESC'])[0];
// Store Activity // Store Activity
$activity = Activity::create([ $activity = Activity::create([
'actor_id' => $subscriber_id, 'actor_id' => $subscriber_id,
'verb' => 'undo', 'verb' => 'undo',
'object_type' => Activity::schemaName(), 'object_type' => 'activity',
'object_id' => $previous_follow_activity->getId(), 'object_id' => $previous_follow_activity->getId(),
'source' => $source, 'source' => $source,
]); ]);
DB::persist($activity); DB::persist($activity);
DB::persist(Attention::create(['object_type' => Activity::schemaName(), 'object_id' => $activity->getId(), 'target_id' => $subscribed_id]));
Event::handle('NewNotification', [ Event::handle('NewNotification', [
\is_int($subject) ? $subject : Actor::getById($subscriber_id), \is_int($subject) ? $subject : Actor::getById($subscriber_id),
$activity, $activity,
[$subscribed_id], ['object' => [$previous_follow_activity->getObjectId()]],
_m('{subject} unsubscribed from {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $previous_follow_activity->getObjectId()]), _m('{subject} unsubscribed from {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $previous_follow_activity->getObjectId()]),
]); ]);
} }
@@ -179,16 +174,17 @@ class Subscription extends Component
* In the case of ``\App\Component\Subscription``, the action added allows a **LocalUser** to **subscribe** or * In the case of ``\App\Component\Subscription``, the action added allows a **LocalUser** to **subscribe** or
* **unsubscribe** a given **Actor**. * **unsubscribe** a given **Actor**.
* *
* @param Actor $object The Actor on which the action is to be performed * @param Actor $object The Actor on which the action is to be performed
* @param array<array{url: string, title: string, classes: string, id: string}> $actions * @param array $actions An array containing all actions added to the
* An array containing all actions added to the * current profile, this event adds an action to it
* current profile, this event adds an action to it
* *
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws NotFoundException * @throws NotFoundException
* @throws ServerException * @throws ServerException
*
* @return bool EventHook
*/ */
public function onAddProfileActions(Request $request, Actor $object, array &$actions): EventResult public function onAddProfileActions(Request $request, Actor $object, array &$actions): bool
{ {
// Action requires a user to be logged in // Action requires a user to be logged in
// We know it's a LocalUser, which has the same id as Actor // We know it's a LocalUser, which has the same id as Actor

View File

@@ -15,12 +15,7 @@ class Tag extends Controller
// TODO: Use Feed::query // TODO: Use Feed::query
// TODO: If ?canonical=something, respect // TODO: If ?canonical=something, respect
// TODO: Allow to set locale of tag being selected // TODO: Allow to set locale of tag being selected
/** private function process(null|string|array $tag_single_or_multi, string $key, string $query, string $template, bool $include_locale = false)
* @param (null|string|string[]) $tag_single_or_multi
*
* @return ControllerResultType
*/
private function process(null|string|array $tag_single_or_multi, string $key, string $query, string $template, bool $include_locale = false): array
{ {
$actor = Common::actor(); $actor = Common::actor();
$page = $this->int('page') ?: 1; $page = $this->int('page') ?: 1;
@@ -51,10 +46,7 @@ class Tag extends Controller
]; ];
} }
/** public function single_note_tag(string $tag)
* @return ControllerResultType
*/
public function single_note_tag(string $tag): array
{ {
return $this->process( return $this->process(
tag_single_or_multi: $tag, tag_single_or_multi: $tag,
@@ -65,10 +57,7 @@ class Tag extends Controller
); );
} }
/** public function multi_note_tags(string $tags)
* @return ControllerResultType
*/
public function multi_note_tags(string $tags): array
{ {
return $this->process( return $this->process(
tag_single_or_multi: explode(',', $tags), tag_single_or_multi: explode(',', $tags),

View File

@@ -22,9 +22,9 @@ declare(strict_types = 1);
namespace Component\Tag\Entity; namespace Component\Tag\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note; use App\Entity\Note;
use Component\Language\Entity\Language; use Component\Language\Entity\Language;
@@ -134,9 +134,6 @@ class NoteTag extends Entity
return "note-tags-{$note_id}"; return "note-tags-{$note_id}";
} }
/**
* @return NoteTag[]
*/
public static function getByNoteId(int $note_id): array public static function getByNoteId(int $note_id): array
{ {
return Cache::getList(self::cacheKey($note_id), fn () => DB::dql('SELECT nt FROM note_tag AS nt JOIN note AS n WITH n.id = nt.note_id WHERE n.id = :id', ['id' => $note_id])); return Cache::getList(self::cacheKey($note_id), fn () => DB::dql('SELECT nt FROM note_tag AS nt JOIN note AS n WITH n.id = nt.note_id WHERE n.id = :id', ['id' => $note_id]));

View File

@@ -22,7 +22,7 @@ declare(strict_types = 1);
namespace Component\Tag\Entity; namespace Component\Tag\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use Component\Tag\Tag; use Component\Tag\Tag;
use DateTimeInterface; use DateTimeInterface;
@@ -119,8 +119,6 @@ class NoteTagBlock extends Entity
/** /**
* Check whether $note_tag is considered blocked by one of * Check whether $note_tag is considered blocked by one of
* $note_tag_blocks * $note_tag_blocks
*
* @param NoteTagBlock[] $note_tag_blocks
*/ */
public static function checkBlocksNoteTag(NoteTag $note_tag, array $note_tag_blocks): bool public static function checkBlocksNoteTag(NoteTag $note_tag, array $note_tag_blocks): bool
{ {

View File

@@ -24,11 +24,11 @@ declare(strict_types = 1);
namespace Component\Tag; namespace Component\Tag;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
@@ -42,7 +42,6 @@ use Component\Tag\Entity\NoteTag;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -61,70 +60,17 @@ class Tag extends Component
public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags
public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,64}'; public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,64}';
public function onAddRoute(Router $r): EventResult public function onAddRoute($r): bool
{ {
$r->connect('single_note_tag', '/note-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']); $r->connect('single_note_tag', '/note-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']);
$r->connect('multi_note_tags', '/note-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']); $r->connect('multi_note_tags', '/note-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']);
return Event::next; return Event::next;
} }
/**
* @param array{tag_use_canonical?: bool} $extra_args
*/
public static function maybeCreateTag(string $tag, int $note_id, ?int $lang_id, array $extra_args = []): ?NoteTag
{
if (!self::validate($tag)) {
return null; // Ignore invalid tag candidates
}
$canonical_tag = self::canonicalTag($tag, \is_null($lang_id) ? null : Language::getById($lang_id)->getLocale());
DB::persist($note_tag = NoteTag::create([
'tag' => $tag,
'canonical' => $canonical_tag,
'note_id' => $note_id,
'use_canonical' => $extra_args['tag_use_canonical'] ?? false,
'language_id' => $lang_id,
]));
foreach (self::cacheKeys($canonical_tag) as $key) {
Cache::delete($key);
}
return $note_tag;
}
/**
* @return NoteTag[]
*/
public static function getNoteTags(int $actor_id, ?string $note_type): array
{
$query = <<<'EOF'
select nt from \App\Entity\Note n
join \Component\Tag\Entity\NoteTag nt with n.id = nt.note_id
where n.actor_id = :id
EOF;
if (\is_null($note_type)) {
return Cache::getList(
Actor::cacheKeys($actor_id, 'any')['note-tags'],
fn () => DB::dql(
$query,
['id' => $actor_id],
),
);
} else {
return Cache::getList(
Actor::cacheKeys($actor_id, $note_type)['note-tags'],
fn () => DB::dql(
$query . ' and n.type = :type',
['id' => $actor_id, 'type' => $note_type],
),
);
}
}
/** /**
* Process note by extracting any tags present * Process note by extracting any tags present
*
* @param array{TagProcessed?: bool} $extra_args
*/ */
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): EventResult public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): bool
{ {
if ($extra_args['TagProcessed'] ?? false) { if ($extra_args['TagProcessed'] ?? false) {
return Event::next; return Event::next;
@@ -136,12 +82,26 @@ class Tag extends Component
$matched_tags = array_unique(F\map($matched_tags, fn ($m) => $m[2])); $matched_tags = array_unique(F\map($matched_tags, fn ($m) => $m[2]));
foreach ($matched_tags as $match) { foreach ($matched_tags as $match) {
$tag = self::extract($match); $tag = self::extract($match);
self::maybeCreateTag(tag: $tag, note_id: $note->getId(), lang_id: $note->getLanguageId()); if (!self::validate($tag)) {
continue; // Ignore invalid tag candidates
}
$canonical_tag = self::canonicalTag($tag, \is_null($lang_id = $note->getLanguageId()) ? null : Language::getById($lang_id)->getLocale());
DB::persist(NoteTag::create([
'tag' => $tag,
'canonical' => $canonical_tag,
'note_id' => $note->getId(),
'use_canonical' => $extra_args['tag_use_canonical'] ?? false,
'language_id' => $lang_id,
]));
Cache::listPushLeft("tag-{$canonical_tag}", $note);
foreach (self::cacheKeys($canonical_tag) as $key) {
Cache::delete($key);
}
} }
return Event::next; return Event::next;
} }
public function onRenderPlainTextNoteContent(string &$text, ?string $locale = null): EventResult public function onRenderPlainTextNoteContent(string &$text, ?string $locale = null): bool
{ {
$text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $locale), $text); $text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $locale), $text);
return Event::next; return Event::next;
@@ -218,11 +178,8 @@ class Tag extends Component
* Populate $note_expr with an expression to match a tag, if the term looks like a tag * Populate $note_expr with an expression to match a tag, if the term looks like a tag
* *
* $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
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool
{ {
if (!str_contains($term, ':')) { if (!str_contains($term, ':')) {
return Event::next; return Event::next;
@@ -261,7 +218,7 @@ class Tag extends Component
return Event::next; return Event::next;
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
{ {
if (!\in_array('note_tag', $note_qb->getAllAliases())) { if (!\in_array('note_tag', $note_qb->getAllAliases())) {
$note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id'); $note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id');
@@ -272,20 +229,13 @@ class Tag extends Component
return Event::next; return Event::next;
} }
/** public function onPostingAddFormEntries(Request $request, Actor $actor, array &$form_params): bool
* @param array{string, class-string, array<string, mixed>} $form_params
*/
public function onPostingAddFormEntries(Request $request, Actor $actor, array &$form_params): EventResult
{ {
$form_params[] = ['tag_use_canonical', CheckboxType::class, ['required' => false, 'data' => true, 'label' => _m('Make note tags canonical'), 'help' => _m('Canonical tags will be treated as a version of an existing tag with the same root/stem (e.g. \'#great_tag\' will be considered as a version of \'#great\', if it already exists)')]]; $form_params[] = ['tag_use_canonical', CheckboxType::class, ['required' => false, 'data' => true, 'label' => _m('Make note tags canonical'), 'help' => _m('Canonical tags will be treated as a version of an existing tag with the same root/stem (e.g. \'#great_tag\' will be considered as a version of \'#great\', if it already exists)')]];
return Event::next; return Event::next;
} }
/** public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args): bool
* @param array{tag_use_canonical?: bool} $data
* @param array{tag_use_canonical?: bool} $extra_args
*/
public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args): EventResult
{ {
if (!isset($data['tag_use_canonical'])) { if (!isset($data['tag_use_canonical'])) {
throw new ClientException(_m('Missing Use Canonical preference for Tags.')); throw new ClientException(_m('Missing Use Canonical preference for Tags.'));

View File

@@ -75,7 +75,7 @@
"friendsofphp/php-cs-fixer": "^3.2.1", "friendsofphp/php-cs-fixer": "^3.2.1",
"jchook/phpunit-assert-throws": "^1.0", "jchook/phpunit-assert-throws": "^1.0",
"niels-de-blaauw/php-doc-check": "^0.2.2", "niels-de-blaauw/php-doc-check": "^0.2.2",
"phpstan/phpstan": "1.9.x-dev", "phpstan/phpstan": "dev-master",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"symfony/browser-kit": "^6.0", "symfony/browser-kit": "^6.0",
"symfony/css-selector": "^6.0", "symfony/css-selector": "^6.0",
@@ -103,9 +103,6 @@
"files": [ "files": [
"src/Core/I18n/I18n.php" "src/Core/I18n/I18n.php"
], ],
"classmap": [
"src/Core/Event/EventResult.php"
],
"psr-4": { "psr-4": {
"App\\": "src/", "App\\": "src/",
"Plugin\\": "plugins/", "Plugin\\": "plugins/",
@@ -115,8 +112,7 @@
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"App\\Test\\Fixtures\\": "tests/fixtures/", "App\\Tests\\": "tests/"
"App\\Test\\": "tests/"
} }
}, },
"replace": { "replace": {

2673
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,13 +20,15 @@ security:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
oauth:
pattern: ^/oauth
security: false
main: main:
lazy: true lazy: true
provider: local_user provider: local_user
form_login: form_login:
login_path: security_login login_path: security_login
check_path: security_login check_path: security_login
default_target_path: root
logout: logout:
path: security_logout path: security_logout
# where to redirect after logout # where to redirect after logout

View File

@@ -13,10 +13,7 @@ services:
# this creates a service per class whose id is the fully-qualified class name # this creates a service per class whose id is the fully-qualified class name
App\: App\:
resource: '../src/*' resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Routes,Core/Event/EventResult.php}' exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Routes}'
App\Test\Fixtures\:
resource: '../tests/fixtures/*'
# controllers are imported separately to make sure services can be injected # controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class # as action arguments even if you don't extend any base controller class
@@ -24,7 +21,7 @@ services:
resource: '../src/Controller' resource: '../src/Controller'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']
App\Core\Router: App\Core\Router\RouteLoader:
tags: ['routing.loader'] tags: ['routing.loader']
# Wrapper around Doctrine's StaticPHP metadata driver # Wrapper around Doctrine's StaticPHP metadata driver

View File

@@ -93,7 +93,7 @@ services:
- ./docker/php/entrypoint.sh:/entrypoint.sh - ./docker/php/entrypoint.sh:/entrypoint.sh
- ./docker/db/wait_for_db.sh:/wait_for_db.sh - ./docker/db/wait_for_db.sh:/wait_for_db.sh
- ./docker/social/install.sh:/var/entrypoint.d/social_install.sh - ./docker/social/install.sh:/var/entrypoint.d/social_install.sh
- ./docker/worker/worker.sh:/var/entrypoint.d/social_worker.sh - ./docker/social/worker.sh:/var/entrypoint.d/social_worker.sh
# Main files # Main files
- .:/var/www/social - .:/var/www/social
- /var/www/social/docker # exclude docker folder - /var/www/social/docker # exclude docker folder

View File

@@ -1,18 +1,3 @@
server {
# Listen only on port 81 for localhost, and nothing else.
server_name 127.0.0.1;
listen 127.0.0.1:81 default_server;
charset utf-8;
# Certbot's folder used for the ACME challenge response.
location ^~ /.well-known/acme-challenge {
default_type text/plain;
root /var/www/certbot;
try_files $uri =404;
}
}
server { server {
listen [::]:80; listen [::]:80;
@@ -20,10 +5,6 @@ server {
server_name %hostname%; server_name %hostname%;
location '/.well-known/acme-challenge' {
proxy_pass http://localhost:81;
}
# redirect all traffic to HTTPS # redirect all traffic to HTTPS
rewrite ^ https://$host$request_uri? permanent; rewrite ^ https://$host$request_uri? permanent;
} }
@@ -54,13 +35,6 @@ server {
root /var/www/social; root /var/www/social;
} }
location /.well-known/acme-challenge/ {
allow all;
root /var/www/certbot;
try_files $uri =404;
break;
}
# PHP # PHP
location ~ ^/(index|install)\.php(/.*)?$ { location ~ ^/(index|install)\.php(/.*)?$ {
include fastcgi_params; include fastcgi_params;

View File

@@ -2,7 +2,8 @@
case "${DBMS}" in case "${DBMS}" in
'postgres') 'postgres')
test "$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -Upostgres -hdb -tAc "select 1 from pg_database where datname='${SOCIAL_DB}'")" = "1" PGPASSWORD="${POSTGRES_PASSWORD}" psql -ltq -Upostgres -hdb | \
cut -d '|' -f1 | grep -Fwq "${SOCIAL_DB}"
DB_EXISTS=$? DB_EXISTS=$?
;; ;;
'mariadb') 'mariadb')
@@ -27,8 +28,7 @@ if [ ${DB_EXISTS} -ne 0 ]; then
chmod g+w -R . chmod g+w -R .
chown -R :www-data . chown -R :www-data .
php bin/console doctrine:database:drop -f php bin/console doctrine:database:create || exit 1
php bin/console doctrine:database:create
php bin/console doctrine:schema:create || exit 1 php bin/console doctrine:schema:create || exit 1
php bin/console app:populate_initial_values || exit 1 php bin/console app:populate_initial_values || exit 1

View File

@@ -3,10 +3,8 @@
cd /var/www/social || exit 1 cd /var/www/social || exit 1
printf "Cleaning Redis cache: " && echo "FLUSHALL" | nc redis 6379 printf "Cleaning Redis cache: " && echo "FLUSHALL" | nc redis 6379
bin/console doctrine:database:drop --force || exit 1 yes yes | php bin/console doctrine:fixtures:load || exit 1
bin/console doctrine:database:create || exit 1 php bin/console app:populate_initial_values # since loading fixtures purges the DB
bin/console doctrine:schema:update --force || exit 1
yes yes | bin/console doctrine:fixtures:load || exit 1
if [ "$#" -eq 0 ] || [ -z "$*" ]; then if [ "$#" -eq 0 ] || [ -z "$*" ]; then
vendor/bin/simple-phpunit -vvv --coverage-html .test_coverage_report vendor/bin/simple-phpunit -vvv --coverage-html .test_coverage_report

View File

@@ -45,10 +45,10 @@ Example 1: Adding elements to the core UI
* @param array $vars Input from the caller/emitter * @param array $vars Input from the caller/emitter
* @param array $res I/O parameter used to accumulate or return values from the listener to the emitter * @param array $res I/O parameter used to accumulate or return values from the listener to the emitter
* *
* @return \EventResult true if not handled or if the handling should be accumulated with other listeners, * @return bool true if not handled or if the handling should be accumulated with other listeners,
* false if handled well enough and no other listeners are needed * false if handled well enough and no other listeners are needed
*/ */
public function onViewAttachmentImage(array $vars, array &$res): \EventResult public function onViewAttachmentImage(array $vars, array &$res): bool
{ {
$res[] = Formatting::twigRenderFile('imageEncoder/imageEncoderView.html.twig', ['attachment' => $vars['attachment'], 'thumbnail_parameters' => $vars['thumbnail_parameters']]); $res[] = Formatting::twigRenderFile('imageEncoder/imageEncoderView.html.twig', ['attachment' => $vars['attachment'], 'thumbnail_parameters' => $vars['thumbnail_parameters']]);
return Event::stop; return Event::stop;
@@ -74,25 +74,11 @@ Event::handle('ResizerAvailable', [&$event_map]);
/** /**
* @param array $event_map output * @param array $event_map output
* *
* @return \EventResult event hook * @return bool event hook
*/ */
public function onResizerAvailable(array &$event_map): \EventResult public function onResizerAvailable(array &$event_map): bool
{ {
$event_map['image'] = 'ResizeImagePath'; $event_map['image'] = 'ResizeImagePath';
return Event::next; return Event::next;
} }
``` ```
Example 3: Default action
-----
An event can be emited to perform an action, but still have a fallback as such:
> Event emitter
```php
if (Event::handle('EventName', $args) !== Event::stop): \EventResult
{
// Do default action, as no-one claimed authority on handling this event
}
```

View File

@@ -1,7 +1,5 @@
parameters: parameters:
level: 6 level: 4
tmpDir: /var/www/social/var/cache/phpstan
inferPrivatePropertyTypeFromConstructor: true
bootstrapFiles: bootstrapFiles:
- config/bootstrap.php - config/bootstrap.php
paths: paths:
@@ -17,13 +15,6 @@ parameters:
earlyTerminatingMethodCalls: earlyTerminatingMethodCalls:
App\Core\Log: App\Core\Log:
- unexpected_exception - unexpected_exception
typeAliases:
ControllerResultType: '(array{_template: string} | array{_redirect: string}) & array<string, mixed>'
CacheKeysType: 'array<string, string>'
SettingsTabsType: 'array<array{title: string, desc: string, id: string, controller: ControllerResultType}>'
OrderByType: "'ASC' | 'DESC' | 'asc' | 'desc'"
ModuleVersionType: 'array{name: string, version: string, author: string, rawdescription: string}'
ignoreErrors: ignoreErrors:
- -
message: '/Access to an undefined property App\\Util\\Bitmap::\$\w+/' message: '/Access to an undefined property App\\Util\\Bitmap::\$\w+/'
@@ -44,41 +35,10 @@ parameters:
paths: paths:
- * - *
- # -
message: '/::onCollectionQueryCreateExpression\(\) has parameter \$(actor|note)_expr with no type specified\./' # message: '/has no return typehint specified/'
paths: # paths:
- * # - tests/*
-
message: '/::schemaDef\(\) return type has no value type specified in iterable type array\./'
paths:
- *
-
message: '/has no return type specified\./'
paths:
- *
-
message: '/with no value type specified in iterable type (array|iterable)\.|type has no value type specified in iterable type (array|iterable)\./'
paths:
- *
-
message: '/never returns array{_redirect: string} so it can be removed from the return type\./'
paths:
- *
-
message: '/but returns array<string, array<int, mixed>\|string>\./'
paths:
- plugins/AttachmentCollections/Controller/AttachmentCollections.php
- plugins/Bundles/Controller/BundleCollection.php
-
message: '/has parameter \$.+ with no type specified./'
paths:
- tests/*
services: services:
- -

View File

@@ -34,14 +34,15 @@ namespace Plugin\ActivityPub;
use ActivityPhp\Type; use ActivityPhp\Type;
use ActivityPhp\Type\AbstractObject; use ActivityPhp\Type\AbstractObject;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\HTTPClient; use App\Core\HTTPClient;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use App\Core\Queue; use App\Core\Queue\Queue;
use App\Core\Router; use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note; use App\Entity\Note;
@@ -50,7 +51,6 @@ use App\Util\Exception\BugFoundException;
use Component\Collection\Util\Controller\OrderedCollection; use Component\Collection\Util\Controller\OrderedCollection;
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol; use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
use Component\FreeNetwork\Util\Discovery; use Component\FreeNetwork\Util\Discovery;
use EventResult;
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use Plugin\ActivityPub\Controller\Inbox; use Plugin\ActivityPub\Controller\Inbox;
@@ -104,7 +104,7 @@ class ActivityPub extends Plugin
'User-Agent' => 'GNUsocialBot ' . GNUSOCIAL_VERSION . ' - ' . GNUSOCIAL_PROJECT_URL, 'User-Agent' => 'GNUsocialBot ' . GNUSOCIAL_VERSION . ' - ' . GNUSOCIAL_PROJECT_URL,
]; ];
public static function version(): string public function version(): string
{ {
return '3.0.0'; return '3.0.0';
} }
@@ -123,14 +123,14 @@ class ActivityPub extends Plugin
], ],
]; ];
public function onInitializePlugin(): EventResult public function onInitializePlugin(): bool
{ {
Event::handle('ActivityStreamsTwoContext', [&self::$activity_streams_two_context]); Event::handle('ActivityStreamsTwoContext', [&self::$activity_streams_two_context]);
self::$activity_streams_two_context = array_unique(self::$activity_streams_two_context, \SORT_REGULAR); self::$activity_streams_two_context = array_unique(self::$activity_streams_two_context, \SORT_REGULAR);
return Event::next; return Event::next;
} }
public function onQueueActivitypubInbox(ActivitypubActor $ap_actor, Actor $actor, string|AbstractObject $type): EventResult public function onQueueActivitypubInbox(ActivitypubActor $ap_actor, Actor $actor, string|AbstractObject $type): bool
{ {
// TODO: Check if Actor has authority over payload // TODO: Check if Actor has authority over payload
@@ -141,12 +141,14 @@ class ActivityPub extends Plugin
$ap_actor->getActorId(), $ap_actor->getActorId(),
Discovery::normalize($actor->getNickname() . '@' . parse_url($ap_actor->getInboxUri(), \PHP_URL_HOST)), Discovery::normalize($actor->getNickname() . '@' . parse_url($ap_actor->getInboxUri(), \PHP_URL_HOST)),
); );
$already_known_ids = [];
if (!empty($ap_act->_object_mention_ids)) {
$already_known_ids = $ap_act->_object_mention_ids;
}
DB::flush(); DB::flush();
if (($att_targets = $ap_act->getAttentionTargets()) !== []) { if (Event::handle('ActivityPubNewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]) === Event::next) {
if (Event::handle('ActivityPubNewNotification', [$actor, ($act = $ap_act->getActivity()), $att_targets, _m('{actor_id} triggered a notification via ActivityPub.', ['{actor_id}' => $actor->getId()])]) === Event::next) { Event::handle('NewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]);
Event::handle('NewNotification', [$actor, $act, $att_targets, _m('{actor_id} triggered a notification via ActivityPub.', ['{nickname}' => $actor->getId()])]);
}
} }
return Event::stop; return Event::stop;
@@ -156,9 +158,9 @@ class ActivityPub extends Plugin
* This code executes when GNU social creates the page routing, and we hook * This code executes when GNU social creates the page routing, and we hook
* on this event to add our Inbox and Outbox handler for ActivityPub. * on this event to add our Inbox and Outbox handler for ActivityPub.
* *
* @param Router $r the router that was initialized * @param RouteLoader $r the router that was initialized
*/ */
public function onAddRoute(Router $r): EventResult public function onAddRoute(RouteLoader $r): bool
{ {
$r->connect( $r->connect(
'activitypub_inbox', 'activitypub_inbox',
@@ -184,7 +186,7 @@ class ActivityPub extends Plugin
/** /**
* Fill Actor->getUrl() calls with correct URL coming from ActivityPub * Fill Actor->getUrl() calls with correct URL coming from ActivityPub
*/ */
public function onStartGetActorUri(Actor $actor, int $type, ?string &$url): EventResult public function onStartGetActorUri(Actor $actor, int $type, ?string &$url): bool
{ {
if ( if (
// Is remote? // Is remote?
@@ -204,7 +206,7 @@ class ActivityPub extends Plugin
/** /**
* Fill Actor->canAdmin() for Actors that came from ActivityPub * Fill Actor->canAdmin() for Actors that came from ActivityPub
*/ */
public function onFreeNetworkActorCanAdmin(Actor $actor, Actor $other, bool &$canAdmin): EventResult public function onFreeNetworkActorCanAdmin(Actor $actor, Actor $other, bool &$canAdmin): bool
{ {
// Are both in AP? // Are both in AP?
if ( if (
@@ -224,7 +226,7 @@ class ActivityPub extends Plugin
* *
* @throws Exception * @throws Exception
*/ */
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): EventResult public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): bool
{ {
if (\count(array_intersect(self::$accept_headers, $accept_header)) === 0) { if (\count(array_intersect(self::$accept_headers, $accept_header)) === 0) {
return Event::next; return Event::next;
@@ -263,7 +265,7 @@ class ActivityPub extends Plugin
/** /**
* Add ActivityStreams 2 Extensions * Add ActivityStreams 2 Extensions
*/ */
public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): EventResult public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): bool
{ {
switch ($type_name) { switch ($type_name) {
case 'Person': case 'Person':
@@ -281,11 +283,9 @@ class ActivityPub extends Plugin
/** /**
* Let FreeNetwork Component know we exist and which class to use to call the freeNetworkDistribute method * Let FreeNetwork Component know we exist and which class to use to call the freeNetworkDistribute method
*/ */
public function onAddFreeNetworkProtocol(array &$protocols): EventResult public function onAddFreeNetworkProtocol(array &$protocols): bool
{ {
if (!\in_array('\Plugin\ActivityPub\ActivityPub', $protocols)) { $protocols[] = '\Plugin\ActivityPub\ActivityPub';
$protocols[] = '\Plugin\ActivityPub\ActivityPub';
}
return Event::next; return Event::next;
} }
@@ -296,11 +296,11 @@ class ActivityPub extends Plugin
* *
* @return bool true if imported, false otherwise * @return bool true if imported, false otherwise
*/ */
public static function freeNetworkGrabRemote(string $uri, ?Actor $on_behalf_of = null): bool public static function freeNetworkGrabRemote(string $uri): bool
{ {
if (Common::isValidHttpUrl($uri)) { if (Common::isValidHttpUrl($uri)) {
try { try {
$object = self::getObjectByUri($uri, try_online: true, on_behalf_of: $on_behalf_of); $object = self::getObjectByUri($uri);
if (!\is_null($object)) { if (!\is_null($object)) {
if ($object instanceof Type\AbstractObject) { if ($object instanceof Type\AbstractObject) {
if (\in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) { if (\in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) {
@@ -324,24 +324,15 @@ class ActivityPub extends Plugin
string $inbox, string $inbox,
array $to_actors, array $to_actors,
array &$retry_args, array &$retry_args,
): EventResult { ): bool
{
try { try {
$data = Model::toType($activity); $data = Model::toJson($activity);
if ($sender->isGroup()) { // When the sender is a group, if ($sender->isGroup()) {
if ($activity->getVerb() === 'subscribe') { // When the sender is a group, we have to wrap it in an Announce activity
// Regular postman happens $data = Type::create('Announce', ['object' => $data])->toJson();
} elseif ($activity->getVerb() === 'undo' && $data->get('object')->get('type') === 'Follow') {
// Regular postman happens
} else {
// For every other activity sent by a Group, we have to wrap it in a transient Announce activity
$data = Type::create('Announce', [
'@context' => 'https:\/\/www.w3.org\/ns\/activitystreams',
'actor' => $sender->getUri(type: Router::ABSOLUTE_URL),
'object' => $data,
]);
}
} }
$res = self::postman($sender, $data->toJson(), $inbox); $res = self::postman($sender, $data, $inbox);
// accumulate errors for later use, if needed // accumulate errors for later use, if needed
$status_code = $res->getStatusCode(); $status_code = $res->getStatusCode();
@@ -389,7 +380,6 @@ class ActivityPub extends Plugin
// the actor, that could for example mean that OStatus handled this actor while we were deactivated // the actor, that could for example mean that OStatus handled this actor while we were deactivated
// On next interaction this should be resolved, for now continue // On next interaction this should be resolved, for now continue
if (\is_null($ap_target = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))) { if (\is_null($ap_target = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))) {
Log::info('FreeNetwork wrongly told ActivityPub that it can handle actor id: ' . $actor->getId() . ' you might want to keep an eye on it.');
continue; continue;
} }
$to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor; $to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor;
@@ -401,7 +391,7 @@ class ActivityPub extends Plugin
foreach ($to_addr as $inbox => $to_actors) { foreach ($to_addr as $inbox => $to_actors) {
Queue::enqueue( Queue::enqueue(
payload: [$sender, $activity, $inbox, $to_actors], payload: [$sender, $activity, $inbox, $to_actors],
queue: 'ActivitypubPostman', queue: 'activitypub_postman',
priority: false, priority: false,
); );
} }
@@ -429,7 +419,7 @@ class ActivityPub extends Plugin
/** /**
* Add activity+json mimetype to WebFinger * Add activity+json mimetype to WebFinger
*/ */
public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): EventResult public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): bool
{ {
if ($object->isPerson()) { if ($object->isPerson()) {
$link = new XML_XRD_Element_Link( $link = new XML_XRD_Element_Link(
@@ -445,7 +435,7 @@ class ActivityPub extends Plugin
/** /**
* When FreeNetwork component asks us to help with identifying Actors from XRDs * When FreeNetwork component asks us to help with identifying Actors from XRDs
*/ */
public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): EventResult public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): bool
{ {
$addr = null; $addr = null;
foreach ($xrd->aliases as $alias) { foreach ($xrd->aliases as $alias) {
@@ -476,7 +466,7 @@ class ActivityPub extends Plugin
/** /**
* When FreeNetwork component asks us to help with identifying Actors from URIs * When FreeNetwork component asks us to help with identifying Actors from URIs
*/ */
public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): EventResult public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): bool
{ {
try { try {
if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) { if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) {
@@ -540,7 +530,7 @@ class ActivityPub extends Plugin
* *
* @return null|Actor|mixed|Note got from URI * @return null|Actor|mixed|Note got from URI
*/ */
public static function getObjectByUri(string $resource, bool $try_online = true, ?Actor $on_behalf_of = null): mixed public static function getObjectByUri(string $resource, bool $try_online = true)
{ {
// Try known object // Try known object
$known_object = DB::findOneBy(ActivitypubObject::class, ['object_uri' => $resource], return_null: true); $known_object = DB::findOneBy(ActivitypubObject::class, ['object_uri' => $resource], return_null: true);
@@ -554,57 +544,39 @@ class ActivityPub extends Plugin
return $known_activity->getActivity(); return $known_activity->getActivity();
} }
// Try Actor // Try local Note
try {
return Explorer::getOneFromUri($resource, try_online: false, on_behalf_of: $on_behalf_of);
} catch (\Exception) {
// Ignore, this is brute forcing, it's okay not to find
}
// Is it a HTTP URL?
if (Common::isValidHttpUrl($resource)) { if (Common::isValidHttpUrl($resource)) {
$resource_parts = parse_url($resource); $resource_parts = parse_url($resource);
// If it is local // TODO: Use URLMatcher
if ($resource_parts['host'] === Common::config('site', 'server')) { if ($resource_parts['host'] === Common::config('site', 'server')) {
// Try Local Note $local_note = DB::findOneBy('note', ['url' => $resource], return_null: true);
$local_note = DB::findOneBy(Note::class, ['url' => $resource], return_null: true);
if (!\is_null($local_note)) { if (!\is_null($local_note)) {
return $local_note; return $local_note;
} }
// Try local Activity
try {
$match = Router::match($resource_parts['path']);
$local_activity = DB::findOneBy(Activity::class, ['id' => $match['id']], return_null: true);
if (!\is_null($local_activity)) {
return $local_activity;
} else {
throw new InvalidArgumentException('Tried to retrieve a non-existent local activity.');
}
} catch (\Exception) {
// Ignore, this is brute forcing, it's okay not to find
}
throw new BugFoundException('ActivityPub failed to retrieve local resource: "' . $resource . '". This is a big issue.');
} else {
// Then it's remote
if (!$try_online) {
throw new Exception("Remote resource {$resource} not found without online resources.");
}
$response = Explorer::get($resource, $on_behalf_of);
// If it was deleted
if ($response->getStatusCode() == 410) {
//$obj = Type::create('Tombstone', ['id' => $resource]);
return null;
} elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
throw new Exception('Non Ok Status Code for given Object id.');
} else {
return Model::jsonToType($response->getContent());
}
} }
} }
return null; // Try Actor
try {
return Explorer::getOneFromUri($resource, try_online: false);
} catch (\Exception) {
// Ignore, this is brute forcing, it's okay not to find
}
// Try remote
if (!$try_online) {
return;
}
$response = HTTPClient::get($resource, ['headers' => self::HTTP_CLIENT_HEADERS]);
// If it was deleted
if ($response->getStatusCode() == 410) {
//$obj = Type::create('Tombstone', ['id' => $resource]);
return;
} elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
throw new Exception('Non Ok Status Code for given Object id.');
} else {
return Model::jsonToType($response->getContent());
}
} }
} }

View File

@@ -32,13 +32,12 @@ declare(strict_types = 1);
namespace Plugin\ActivityPub\Controller; namespace Plugin\ActivityPub\Controller;
use ActivityPhp\Type\AbstractObject;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB\DB;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Queue; use App\Core\Queue\Queue;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
@@ -97,12 +96,10 @@ class Inbox extends Controller
return $error('Actor not found in the request.'); return $error('Actor not found in the request.');
} }
$to_actor = $this->deriveActivityTo($type);
try { try {
$resource_parts = parse_url($type->get('actor')); $resource_parts = parse_url($type->get('actor'));
if ($resource_parts['host'] !== Common::config('site', 'server')) { if ($resource_parts['host'] !== Common::config('site', 'server')) {
$actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor'), try_online: true, on_behalf_of: $to_actor)); $actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor')));
$ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]); $ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]);
} else { } else {
throw new Exception('Only remote actors can use this endpoint.'); throw new Exception('Only remote actors can use this endpoint.');
@@ -140,7 +137,7 @@ class Inbox extends Controller
// If the signature fails verification the first time, update profile as it might have changed public key // If the signature fails verification the first time, update profile as it might have changed public key
if ($verified !== 1) { if ($verified !== 1) {
try { try {
$res = Explorer::getRemoteActorActivity($ap_actor->getUri(), $to_actor); $res = Explorer::getRemoteActorActivity($ap_actor->getUri());
if (\is_null($res)) { if (\is_null($res)) {
return $error('Invalid remote actor (null response).'); return $error('Invalid remote actor (null response).');
} }
@@ -167,74 +164,10 @@ class Inbox extends Controller
Queue::enqueue( Queue::enqueue(
payload: [$ap_actor, $actor, $type], payload: [$ap_actor, $actor, $type],
queue: 'ActivitypubInbox', queue: 'activitypub_inbox',
priority: false, priority: false,
); );
return new TypeResponse($type, status: 202); return new TypeResponse($type, status: 202);
} }
/**
* Poke at the given AbstractObject to find out who it is 'to'.
* Function will check through the 'to', 'cc', and 'object' fields
* of the given type (in that order) to check if if points to anyone
* on our instance. The first found candidate will be returned.
*
* @param AbstractObject $type
*
* @return Actor|null The discovered actor, if found. null otherwise.
*
* @throws Exception
*/
private function deriveActivityTo(AbstractObject $type): Actor|null
{
foreach (['to', 'cc'] as $field) {
foreach ((array) $type->get($field) as $uri) {
$actor = self::uriToMaybeLocalActor($uri);
if (!\is_null($actor)) {
return $actor;
}
}
}
// if it's not to or cc anyone we have to dive deeper
if ($type->has('object')) {
// the 'object' field might just be a uri of one
// of our Actors, if this is a follow or block
$object = $type->get('object');
if (\is_string($object)) {
$actor = self::uriToMaybeLocalActor($object);
if (!\is_null($actor)) {
return $actor;
}
} else if ($object instanceof AbstractObject) {
// if the inner object is also a Type, repeat the process
return $this->deriveActivityTo($object);
}
}
return null;
}
/**
* Get local actor that owns or corresponds to given uri.
*
* @param string $uri
*
* @return Actor|null
*/
private static function uriToMaybeLocalActor(string $uri): Actor|null
{
$parsed = parse_url($uri);
// check if this uri belongs to us
if ($parsed['host'] === Common::config('site', 'server')) {
// it is our uri so we should be able to get
// the actor without making any remote calls
$actor = Explorer::getLocalActorForPath($parsed['path']);
if (!\is_null($actor)) {
return $actor;
}
}
return null;
}
} }

View File

@@ -34,7 +34,7 @@ namespace Plugin\ActivityPub\Controller;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Router; use App\Core\Router\Router;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;

View File

@@ -32,7 +32,7 @@ declare(strict_types = 1);
namespace Plugin\ActivityPub\Entity; namespace Plugin\ActivityPub\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Activity; use App\Entity\Activity;
use DateTimeInterface; use DateTimeInterface;
@@ -101,17 +101,27 @@ class ActivitypubActivity extends Entity
public function getActivity(): Activity public function getActivity(): Activity
{ {
return DB::findOneBy(Activity::class, ['id' => $this->getActivityId()]); return DB::findOneBy('activity', ['id' => $this->getActivityId()]);
} }
public function getAttentionTargetIds(): array public array $_object_mention_ids = [];
public function setObjectMentionIds(array $mentions): self
{ {
return $this->getActivity()->getAttentionTargetIds(); $this->_object_mention_ids = $mentions;
return $this;
} }
public function getAttentionTargets(): array /**
* @see Entity->getNotificationTargetIds
*/
public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array
{ {
return $this->getActivity()->getAttentionTargets(); // Additional actors that should know about this
if (\array_key_exists('additional', $ids_already_known)) {
return $ids_already_known['additional'];
} else {
return $this->_object_mention_ids;
}
} }
public static function schemaDef(): array public static function schemaDef(): array

View File

@@ -33,7 +33,7 @@ declare(strict_types = 1);
namespace Plugin\ActivityPub\Entity; namespace Plugin\ActivityPub\Entity;
use App\Core\Cache; use App\Core\Cache;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;

View File

@@ -32,7 +32,7 @@ declare(strict_types = 1);
namespace Plugin\ActivityPub\Entity; namespace Plugin\ActivityPub\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use DateTimeInterface; use DateTimeInterface;
@@ -115,16 +115,6 @@ class ActivitypubObject extends Entity
return DB::findOneBy($this->getObjectType(), ['id' => $this->getObjectId()]); return DB::findOneBy($this->getObjectType(), ['id' => $this->getObjectId()]);
} }
public function getAttentionTargetIds(): array
{
return $this->getObject()->getAttentionTargetIds();
}
public function getAttentionTargets(): array
{
return $this->getObject()->getAttentionTargets();
}
public static function schemaDef(): array public static function schemaDef(): array
{ {
return [ return [

View File

@@ -32,7 +32,7 @@ declare(strict_types = 1);
namespace Plugin\ActivityPub\Entity; namespace Plugin\ActivityPub\Entity;
use App\Core\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Log; use App\Core\Log;
use App\Entity\Actor; use App\Entity\Actor;

View File

@@ -1,70 +0,0 @@
<?php
declare(strict_types = 1);
namespace Plugin\ActivityPub\Test\Fixtures;
use App\Core\DB;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Plugin\ActivityPub\Util\Model\Activity;
use Plugin\ActivityPub\Util\Model\Actor;
use Plugin\ActivityPub\Util\Model\Note;
class ActivityPubFixtures extends Fixture
{
private static string $fixtures_path = __DIR__ . \DIRECTORY_SEPARATOR;
public static function fixturesPath(string $path, string $ontology = 'gnusocial'): string
{
return self::$fixtures_path . $ontology . \DIRECTORY_SEPARATOR . $path;
}
public function load(ObjectManager $manager)
{
/*
* Beware that it's important to Load Actors, Objects, Activities in this sequence
* because we're running offline tests here.
*/
$ontology = 'gnusocial';
// Load Actors
$person_path = self::fixturesPath('objects/person.jsonld', $ontology);
$person = Actor::fromJson(fread(fopen($person_path, 'r'), filesize($person_path)));
DB::flush();
$another_person_path = self::fixturesPath('objects/another_person.jsonld', $ontology);
$another_person = Actor::fromJson(fread(fopen($another_person_path, 'r'), filesize($another_person_path)));
DB::flush();
$group_path = self::fixturesPath('objects/group.jsonld', $ontology);
$group = Actor::fromJson(fread(fopen($group_path, 'r'), filesize($group_path)));
DB::flush();
// Load Objects
$note_path = self::fixturesPath('objects/note.jsonld', $ontology);
$note = Note::fromJson(fread(fopen($note_path, 'r'), filesize($note_path)));
DB::flush();
$article_path = self::fixturesPath('objects/article.jsonld', $ontology);
$article = Note::fromJson(fread(fopen($article_path, 'r'), filesize($article_path)));
DB::flush();
$reply_path = self::fixturesPath('objects/reply.jsonld', $ontology);
$reply = Note::fromJson(fread(fopen($reply_path, 'r'), filesize($reply_path)));
DB::flush();
$note_with_mention_path = self::fixturesPath('objects/note_with_mention.jsonld', $ontology);
$note_with_mention = Note::fromJson(fread(fopen($note_with_mention_path, 'r'), filesize($note_with_mention_path)));
DB::flush();
// Load Activities
$create_note_path = self::fixturesPath('activities/create_note.jsonld', $ontology);
$create_note = Activity::fromJson(fread(fopen($create_note_path, 'r'), filesize($create_note_path)));
DB::flush();
$create_article_path = self::fixturesPath('activities/create_article.jsonld', $ontology);
$create_article = Activity::fromJson(fread(fopen($create_article_path, 'r'), filesize($create_article_path)));
DB::flush();
$create_reply_path = self::fixturesPath('activities/create_reply.jsonld', $ontology);
$create_reply = Activity::fromJson(fread(fopen($create_reply_path, 'r'), filesize($create_reply_path)));
DB::flush();
$like_note_path = self::fixturesPath('activities/like_note.jsonld', $ontology);
$like_note = Activity::fromJson(fread(fopen($like_note_path, 'r'), filesize($like_note_path)));
DB::flush();
}
}

View File

@@ -1,53 +0,0 @@
{
"type": "Create",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"gs": "https://www.gnu.org/software/social/ns#"
},
{
"litepub": "http://litepub.social/ns#"
},
{
"chatMessage": "litepub:chatMessage"
},
{
"inConversation": {
"@id": "gs:inConversation",
"@type": "@id"
}
}
],
"id": "https://instance.gnusocial.test/activity/1338",
"published": "2022-03-17T23:30:26+00:00",
"actor": "https://instance.gnusocial.test/actor/42",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://instance.gnusocial.test/actor/21"
],
"object": {
"type": "Article",
"id": "https://instance.gnusocial.test/object/note/1338",
"published": "2022-03-17T23:30:26+00:00",
"attributedTo": "https://instance.gnusocial.test/actor/42",
"name": "hello, world.",
"content": "<p>This is an interesting article.</p>",
"mediaType": "text/html",
"source": {
"content": "This is an interesting article.",
"mediaType": "text/markdown"
},
"attachment": [],
"tag": [],
"inConversation": "https://instance.gnusocial.test/conversation/1338",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://instance.gnusocial.test/actor/21"
]
}
}

View File

@@ -1,53 +0,0 @@
{
"type": "Create",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"gs": "https://www.gnu.org/software/social/ns#"
},
{
"litepub": "http://litepub.social/ns#"
},
{
"chatMessage": "litepub:chatMessage"
},
{
"inConversation": {
"@id": "gs:inConversation",
"@type": "@id"
}
}
],
"id": "https://instance.gnusocial.test/activity/1339",
"published": "2022-03-01T20:58:48+00:00",
"actor": "https://instance.gnusocial.test/actor/42",
"object": {
"type": "Note",
"id": "https://instance.gnusocial.test/object/note/1339",
"published": "2022-03-01T21:00:16+00:00",
"attributedTo": "https://instance.gnusocial.test/actor/42",
"content": "<p>yay ^^</p>",
"mediaType": "text/html",
"source": {
"content": "yay ^^",
"mediaType": "text/plain"
},
"attachment": [],
"tag": [],
"inReplyTo": "https://instance.gnusocial.test/object/note/1338",
"inConversation": "https://instance.gnusocial.test/conversation/1338",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://instance.gnusocial.test/actor/42/subscribers"
]
},
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://instance.gnusocial.test/actor/42/subscribers"
]
}

Some files were not shown because too many files have changed in this diff Show More