forked from GNUsocial/gnu-social
Compare commits
91 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
49a80a3c40 | ||
|
97114e38e0
|
||
|
2df30e2987
|
||
|
3b3ded5212
|
||
|
dc240fae49
|
||
5cbb1627f2
|
|||
46ff8aacd2
|
|||
c4d6df4637
|
|||
053bc38792
|
|||
2fd46ca886
|
|||
c31f3d4997
|
|||
|
e6bb418fe6
|
||
fed2242a56
|
|||
edeee49af9
|
|||
4d7742e0e1
|
|||
76f2cdd212
|
|||
a2aa45fb1f
|
|||
d4b7e990ce
|
|||
aef1fac536
|
|||
556ac85061
|
|||
539104ec33
|
|||
74ffd261b8
|
|||
ca9945a4be
|
|||
08587b6942
|
|||
1664293cf7
|
|||
94ab4ce8c4
|
|||
dd70de20da
|
|||
ded9c86054
|
|||
20e07c9140
|
|||
4e2f6545ec
|
|||
f6a8f44420
|
|||
fd71d6ee7d
|
|||
dfc5918c2c
|
|||
83599ef866
|
|||
fa82306f6f
|
|||
10f71e9fed
|
|||
e2501ee927
|
|||
a9665177ea
|
|||
41861d284c
|
|||
bd868a2675
|
|||
87e35716c1
|
|||
dac94f53cd
|
|||
b10c359dec
|
|||
483983790a
|
|||
60af9f5e9b
|
|||
abe35428da
|
|||
ca5520edbf
|
|||
e3e14c53ef
|
|||
be33c20614
|
|||
7305a725cb
|
|||
fd4c3b0e68
|
|||
16f51e5143
|
|||
ba4230447e
|
|||
7463044971
|
|||
7027633ed5
|
|||
48b42c539c
|
|||
d41a67a9f9
|
|||
13f22c911c
|
|||
56b8710b26
|
|||
e63c310d70
|
|||
03f449035a
|
|||
8808195a80
|
|||
45344c80d1
|
|||
7eddbd343d
|
|||
259d2da05a
|
|||
2f7fdf6ee4
|
|||
6955872e05
|
|||
23e88b30a6
|
|||
60713878f0
|
|||
06c67b31c2
|
|||
a08b661779
|
|||
0649a5154c
|
|||
91fecd77ba
|
|||
e22fe55bbe
|
|||
dd62825169
|
|||
27706d63f4
|
|||
20f690c532
|
|||
888c3798b7
|
|||
e1cceac150
|
|||
63ef9292f3
|
|||
cbae649991
|
|||
1d8bba3949
|
|||
18864ca9fa
|
|||
390c532456
|
|||
636cb681d6
|
|||
7d84323df4
|
|||
2d7850ccfb
|
|||
d8108dbc32
|
|||
cf05d3dbb0
|
|||
eb3c848fc8
|
|||
5c708af272
|
@@ -3,4 +3,4 @@ KERNEL_CLASS='App\Kernel'
|
||||
APP_SECRET='$ecretf0rt3st'
|
||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
DATABASE_URL=postgresql://postgres:password@db:5432/social
|
||||
DATABASE_URL=postgresql://postgres:password@db:5432/test
|
||||
|
@@ -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.
|
||||
'no_spaces_inside_parenthesis' => true,
|
||||
// Removes `@param`, `@return` and `@var` tags that don't provide any useful information.
|
||||
'no_superfluous_phpdoc_tags' => true,
|
||||
'no_superfluous_phpdoc_tags' => false,
|
||||
// Remove trailing commas in list function calls.
|
||||
'no_trailing_comma_in_list_call' => true,
|
||||
// PHP single-line arrays should not have trailing comma.
|
||||
|
@@ -22,10 +22,10 @@ declare(strict_types = 1);
|
||||
namespace Component\Attachment;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Formatting;
|
||||
@@ -34,10 +34,11 @@ use Component\Attachment\Entity as E;
|
||||
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use EventResult;
|
||||
|
||||
class Attachment extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$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']);
|
||||
@@ -51,13 +52,13 @@ class Attachment extends Component
|
||||
*
|
||||
* This can be used in the future to deduplicate images by visual content
|
||||
*/
|
||||
public function onHashFile(string $filename, ?string &$out_hash): bool
|
||||
public function onHashFile(string $filename, ?string &$out_hash): EventResult
|
||||
{
|
||||
$out_hash = hash_file(E\Attachment::FILEHASH_ALGO, $filename);
|
||||
return Event::stop;
|
||||
}
|
||||
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
|
||||
{
|
||||
Cache::delete("note-attachments-{$note->getId()}");
|
||||
foreach ($note->getAttachments() as $attachment) {
|
||||
@@ -68,7 +69,7 @@ class Attachment extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
|
||||
{
|
||||
if (!\in_array('attachment_to_note', $note_qb->getAllAliases())) {
|
||||
$note_qb->leftJoin(
|
||||
@@ -84,7 +85,7 @@ class Attachment extends Component
|
||||
/**
|
||||
* 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): bool
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
|
||||
{
|
||||
$include_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;
|
||||
if (Formatting::startsWith($term, ['note-types:', 'notes-incude:', 'note-filter:'])) {
|
||||
|
@@ -24,7 +24,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Attachment\Controller;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use function App\Core\I18n\_m;
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Attachment\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
|
@@ -24,13 +24,13 @@ declare(strict_types = 1);
|
||||
namespace Component\Attachment\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
|
@@ -24,12 +24,12 @@ declare(strict_types = 1);
|
||||
namespace Component\Attachment\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Attachment\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Attachment\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
|
@@ -23,7 +23,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Attachment\tests\Controller;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use Component\Attachment\Entity\Attachment;
|
||||
use Component\Attachment\Entity\AttachmentToNote;
|
||||
|
@@ -21,10 +21,10 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Attachment\tests\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Note;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use App\Util\TemporaryFile;
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Attachment\tests\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Util\Exception\NotStoredLocallyException;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
|
@@ -22,26 +22,27 @@ declare(strict_types = 1);
|
||||
namespace Component\Avatar;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Util\Common;
|
||||
use Component\Attachment\Entity\Attachment;
|
||||
use Component\Attachment\Entity\AttachmentThumbnail;
|
||||
use Component\Avatar\Controller as C;
|
||||
use Component\Avatar\Exception\NoAvatarException;
|
||||
use EventResult;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Avatar extends Component
|
||||
{
|
||||
public function onInitializeComponent()
|
||||
public function onInitializeComponent(): EventResult
|
||||
{
|
||||
return EventResult::next;
|
||||
}
|
||||
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$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']);
|
||||
@@ -50,9 +51,11 @@ class Avatar extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SettingsTabsType $tabs
|
||||
*
|
||||
* @throws \App\Util\Exception\ClientException
|
||||
*/
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): bool
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): EventResult
|
||||
{
|
||||
if ($section === 'profile') {
|
||||
$tabs[] = [
|
||||
@@ -65,7 +68,7 @@ class Avatar extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onAvatarUpdate(int $actor_id): bool
|
||||
public function onAvatarUpdate(int $actor_id): EventResult
|
||||
{
|
||||
Cache::delete("avatar-{$actor_id}");
|
||||
foreach (['full', 'big', 'medium', 'small'] as $size) {
|
||||
@@ -128,6 +131,8 @@ class Avatar extends Component
|
||||
*
|
||||
* Returns the avatar file's hash, mimetype, title and path.
|
||||
* 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
|
||||
{
|
||||
|
@@ -24,7 +24,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Avatar\Controller;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use App\Core\GSFile;
|
||||
|
@@ -24,10 +24,10 @@ declare(strict_types = 1);
|
||||
namespace Component\Avatar\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Util\Common;
|
||||
use Component\Attachment\Entity\Attachment;
|
||||
use Component\Attachment\Entity\AttachmentThumbnail;
|
||||
|
@@ -24,16 +24,14 @@ declare(strict_types = 1);
|
||||
namespace Component\Circle;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Feed;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Common;
|
||||
use App\Util\Nickname;
|
||||
use Component\Circle\Controller as CircleController;
|
||||
use Component\Circle\Entity\ActorCircle;
|
||||
@@ -41,6 +39,7 @@ use Component\Circle\Entity\ActorCircleSubscription;
|
||||
use Component\Circle\Entity\ActorTag;
|
||||
use Component\Collection\Util\MetaCollectionTrait;
|
||||
use Component\Tag\Tag;
|
||||
use EventResult;
|
||||
use Functional as F;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -55,12 +54,13 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
*/
|
||||
class Circle extends Component
|
||||
{
|
||||
/** @phpstan-use MetaCollectionTrait<ActorCircle> */
|
||||
use MetaCollectionTrait;
|
||||
public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/';
|
||||
protected const SLUG = 'circle';
|
||||
protected const PLURAL_SLUG = 'circles';
|
||||
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$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
|
||||
@@ -95,20 +95,23 @@ class Circle extends Component
|
||||
];
|
||||
}
|
||||
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): EventResult
|
||||
{
|
||||
if ($section === 'profile' && \in_array($request->get('_route'), ['person_actor_settings', 'group_actor_settings'])) {
|
||||
$tabs[] = [
|
||||
'title' => 'Self tags',
|
||||
'desc' => 'Add or remove tags on yourself',
|
||||
'title' => _m('Self tags'),
|
||||
'desc' => _m('Add or remove tags to this actor'),
|
||||
'id' => 'settings-self-tags',
|
||||
'controller' => CircleController\SelfTagsSettings::settingsSelfTags($request, Common::actor(), 'settings-self-tags-details'),
|
||||
'controller' => CircleController\SelfTagsSettings::settingsSelfTags($request, Actor::getById((int) $request->get('id')), 'settings-self-tags-details'),
|
||||
];
|
||||
}
|
||||
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();
|
||||
foreach ($circles as $circle) {
|
||||
@@ -120,6 +123,9 @@ class Circle extends Component
|
||||
|
||||
// Meta Collection -------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
*/
|
||||
private function getActorIdFromVars(array $vars): int
|
||||
{
|
||||
$id = $vars['request']->get('id', null);
|
||||
@@ -131,7 +137,7 @@ class Circle extends Component
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
public static function createCircle(Actor|int $tagger_id, string $tag): int
|
||||
public static function createCircle(Actor|int $tagger_id, string $tag): int|null
|
||||
{
|
||||
$tagger_id = \is_int($tagger_id) ? $tagger_id : $tagger_id->getId();
|
||||
$circle = ActorCircle::create([
|
||||
@@ -147,7 +153,10 @@ class Circle extends Component
|
||||
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);
|
||||
DB::persist(ActorTag::create([
|
||||
@@ -157,7 +166,12 @@ 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();
|
||||
$tagged_id = $this->getActorIdFromVars($vars);
|
||||
@@ -170,9 +184,15 @@ class Circle extends Component
|
||||
DB::removeBy(ActorTag::class, ['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]);
|
||||
}
|
||||
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();
|
||||
$tagged_id = $this->getActorIdFromVars($vars);
|
||||
@@ -189,8 +209,10 @@ class Circle extends Component
|
||||
|
||||
/**
|
||||
* @see MetaCollectionPlugin->shouldAddToRightPanel
|
||||
*
|
||||
* @param array<string, mixed> $vars
|
||||
*/
|
||||
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool
|
||||
protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool
|
||||
{
|
||||
return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id']);
|
||||
}
|
||||
@@ -203,8 +225,10 @@ class Circle extends Component
|
||||
* 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 null|array $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @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
|
||||
{
|
||||
@@ -220,7 +244,7 @@ class Circle extends Component
|
||||
return $ids_only ? array_map(fn ($x) => $x->getId(), $circles) : $circles;
|
||||
}
|
||||
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering)
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
|
||||
{
|
||||
DB::persist(Feed::create([
|
||||
'actor_id' => $actor_id,
|
||||
|
@@ -38,6 +38,8 @@ class Circle extends CircleController
|
||||
*
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
* @throws ClientException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function circleById(int|ActorCircle $circle_id): array
|
||||
{
|
||||
@@ -57,11 +59,17 @@ class Circle extends CircleController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function circleByTaggerIdAndTag(int $tagger_id, string $tag): array
|
||||
{
|
||||
return $this->circleById(ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function circleByTaggerNicknameAndTag(string $tagger_nickname, string $tag): array
|
||||
{
|
||||
return $this->circleById(ActorCircle::getByPK(['tagger' => LocalUser::getByNickname($tagger_nickname)->getId(), 'tag' => $tag]));
|
||||
|
@@ -24,23 +24,27 @@ declare(strict_types = 1);
|
||||
namespace Component\Circle\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\DB;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use Component\Circle\Entity\ActorCircle;
|
||||
use Component\Collection\Util\Controller\MetaCollectionController;
|
||||
|
||||
/**
|
||||
* @extends MetaCollectionController<Circles>
|
||||
*/
|
||||
class Circles extends MetaCollectionController
|
||||
{
|
||||
protected const SLUG = 'circle';
|
||||
protected const PLURAL_SLUG = 'circles';
|
||||
protected string $page_title = 'Actor circles';
|
||||
|
||||
public function createCollection(int $owner_id, string $name)
|
||||
public function createCollection(int $owner_id, string $name): bool
|
||||
{
|
||||
return \Component\Circle\Circle::createCircle($owner_id, $name);
|
||||
return !\is_null(\Component\Circle\Circle::createCircle($owner_id, $name));
|
||||
}
|
||||
|
||||
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
|
||||
{
|
||||
return Router::url(
|
||||
@@ -49,21 +53,26 @@ 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
|
||||
{
|
||||
$notes = []; // TODO: Use Feed::query
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'notes' => $notes,
|
||||
];
|
||||
return []; // TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Circles[]
|
||||
*/
|
||||
public function feedByCircleId(int $circle_id)
|
||||
{
|
||||
// Owner id isn't used
|
||||
return $this->getCollectionItems(0, $circle_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Circles[]
|
||||
*/
|
||||
public function feedByTaggerIdAndTag(int $tagger_id, string $tag)
|
||||
{
|
||||
// Owner id isn't used
|
||||
@@ -71,6 +80,9 @@ class Circles extends MetaCollectionController
|
||||
return $this->getCollectionItems($tagger_id, $circle_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Circles[]
|
||||
*/
|
||||
public function feedByTaggerNicknameAndTag(string $tagger_nickname, string $tag)
|
||||
{
|
||||
$tagger_id = LocalUser::getByNickname($tagger_nickname)->getId();
|
||||
@@ -82,12 +94,13 @@ class Circles extends MetaCollectionController
|
||||
{
|
||||
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]);
|
||||
}
|
||||
|
||||
public function setCollectionName(int $actor_id, string $actor_nickname, ActorCircle $collection, string $name)
|
||||
public function setCollectionName(int $actor_id, string $actor_nickname, ActorCircle $collection, string $name): void
|
||||
{
|
||||
foreach ($collection->getActorTags(db_reference: true) as $at) {
|
||||
$at->setTag($name);
|
||||
@@ -96,7 +109,7 @@ class Circles extends MetaCollectionController
|
||||
Cache::delete(Actor::cacheKeys($actor_id)['circles']);
|
||||
}
|
||||
|
||||
public function removeCollection(int $actor_id, string $actor_nickname, ActorCircle $collection)
|
||||
public function removeCollection(int $actor_id, string $actor_nickname, ActorCircle $collection): void
|
||||
{
|
||||
foreach ($collection->getActorTags(db_reference: true) as $at) {
|
||||
DB::remove($at);
|
||||
|
@@ -6,7 +6,7 @@ namespace Component\Circle\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity as E;
|
||||
use App\Util\Common;
|
||||
@@ -45,7 +45,7 @@ class SelfTagsSettings extends Controller
|
||||
foreach ($tags as $tag) {
|
||||
$tag = CompTag::sanitize($tag);
|
||||
|
||||
[$actor_tag, $actor_tag_existed] = ActorTag::createOrUpdate([
|
||||
[$actor_tag, $actor_tag_existed] = ActorTag::checkExistingAndCreateOrUpdate([
|
||||
'tagger' => $target->getId(), // self tag means tagger = tagger in ActorTag
|
||||
'tagged' => $target->getId(),
|
||||
'tag' => $tag,
|
||||
|
@@ -22,9 +22,10 @@ declare(strict_types = 1);
|
||||
namespace Component\Circle\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
@@ -144,6 +145,9 @@ class ActorCircle extends Entity
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ActorTag[]
|
||||
*/
|
||||
public function getActorTags(bool $db_reference = false): array
|
||||
{
|
||||
$handle = fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'tag' => $this->getTag()]);
|
||||
@@ -156,7 +160,10 @@ class ActorCircle extends Entity
|
||||
);
|
||||
}
|
||||
|
||||
public function getTaggedActors()
|
||||
/**
|
||||
* @return Actor[]
|
||||
*/
|
||||
public function getTaggedActors(): array
|
||||
{
|
||||
return Cache::get(
|
||||
"circle-{$this->getId()}-tagged-actors",
|
||||
@@ -170,6 +177,9 @@ class ActorCircle extends Entity
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Actor[]
|
||||
*/
|
||||
public function getSubscribedActors(?int $offset = null, ?int $limit = null): array
|
||||
{
|
||||
return Cache::get(
|
||||
|
@@ -21,9 +21,9 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Circle\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use Component\Tag\Tag;
|
||||
use DateTimeInterface;
|
||||
|
@@ -7,14 +7,18 @@ namespace Component\Circle\Form;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
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\TextType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
abstract class SelfTagsForm
|
||||
{
|
||||
/**
|
||||
* @return array [Form (add), ?Form (existing)]
|
||||
* @param ActorTag[] $actor_self_tags
|
||||
*
|
||||
* @return array{FormInterface, ?FormInterface} [Form (add), ?Form (existing)]
|
||||
*/
|
||||
public static function handleTags(
|
||||
Request $request,
|
||||
@@ -34,7 +38,7 @@ abstract class SelfTagsForm
|
||||
$existing_form = !empty($form_definition) ? Form::create($form_definition) : null;
|
||||
|
||||
$add_form = Form::create([
|
||||
['new-tags', TextType::class, ['label' => ' ', 'data' => [], 'required' => false, 'help' => _m('Tags for yourself (letters, numbers, -, ., and _), comma- or space-separated.'), 'transformer' => ArrayTransformer::class]],
|
||||
['new-tags', TextType::class, ['label' => ' ', 'data' => [], 'required' => false, 'help' => _m('Tags for this actor (letters, numbers, -, ., and _), comma- or space-separated.'), 'transformer' => ArrayTransformer::class]],
|
||||
[$add_form_name = 'new-tags-add', SubmitType::class, ['label' => $add_label]],
|
||||
]);
|
||||
|
||||
|
@@ -4,7 +4,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Collection;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Entity\Actor;
|
||||
@@ -14,6 +14,7 @@ use Component\Subscription\Entity\ActorSubscription;
|
||||
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use EventResult;
|
||||
|
||||
class Collection extends Component
|
||||
{
|
||||
@@ -22,6 +23,11 @@ class Collection extends Component
|
||||
*
|
||||
* Supports a variety of query terms and is used both in feeds and
|
||||
* 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
|
||||
{
|
||||
@@ -64,7 +70,7 @@ class Collection extends Component
|
||||
return ['notes' => $notes ?? null, 'actors' => $actors ?? null];
|
||||
}
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
|
||||
{
|
||||
$note_aliases = $note_qb->getAllAliases();
|
||||
if (!\in_array('subscription', $note_aliases)) {
|
||||
@@ -79,8 +85,11 @@ class Collection extends Component
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param mixed $note_expr
|
||||
* @param mixed $actor_expr
|
||||
*/
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr)
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
|
||||
{
|
||||
if (str_contains($term, ':')) {
|
||||
$term = explode(':', $term);
|
||||
|
@@ -4,6 +4,9 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Collection\Util\Controller;
|
||||
|
||||
/**
|
||||
* @extends OrderedCollection<\Component\Circle\Entity\ActorCircle>
|
||||
*/
|
||||
class CircleController extends OrderedCollection
|
||||
{
|
||||
}
|
||||
|
@@ -6,15 +6,25 @@ namespace Component\Collection\Util\Controller;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use Component\Collection\Collection as CollectionModule;
|
||||
use Component\Collection\Collection as CollectionComponent;
|
||||
|
||||
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
|
||||
{
|
||||
$actor ??= Common::actor();
|
||||
$locale ??= Common::currentLanguage()->getLocale();
|
||||
return CollectionModule::query($query, $this->int('page') ?? 1, $locale, $actor, $note_order_by, $actor_order_by);
|
||||
return CollectionComponent::query($query, $this->int('page') ?? 1, $locale, $actor, $note_order_by, $actor_order_by);
|
||||
}
|
||||
}
|
||||
|
@@ -38,12 +38,23 @@ use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use Functional as F;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends OrderedCollection<T>
|
||||
*/
|
||||
abstract class FeedController extends OrderedCollection
|
||||
{
|
||||
/**
|
||||
* Post-processing of the result of a feed controller, to remove any
|
||||
* notes or actors the user specified, as well as format the raw
|
||||
* list of notes into a usable format
|
||||
*
|
||||
* @template NA of Note|Actor
|
||||
*
|
||||
* @param NA[] $result
|
||||
*
|
||||
* @return NA[]
|
||||
*/
|
||||
protected function postProcess(array $result): array
|
||||
{
|
||||
@@ -58,6 +69,9 @@ abstract class FeedController extends OrderedCollection
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note[] $notes
|
||||
*/
|
||||
private static function enforceScope(array &$notes, ?Actor $actor, ?Actor $in = null): void
|
||||
{
|
||||
$notes = F\select($notes, fn (Note $n) => $n->isVisibleTo($actor, $in));
|
||||
|
@@ -31,7 +31,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Collection\Util\Controller;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity\LocalUser;
|
||||
@@ -39,8 +39,14 @@ use App\Util\Common;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @extends FeedController<T>
|
||||
*/
|
||||
abstract class MetaCollectionController extends FeedController
|
||||
{
|
||||
protected const SLUG = 'collectionsEntry';
|
||||
@@ -48,17 +54,36 @@ abstract class MetaCollectionController extends FeedController
|
||||
protected string $page_title = 'Collections';
|
||||
|
||||
abstract public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string;
|
||||
abstract public function getCollectionItems(int $owner_id, $collection_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[]
|
||||
*/
|
||||
abstract public function getCollectionItems(int $owner_id, int $collection_id): array;
|
||||
|
||||
/**
|
||||
* @return T[]
|
||||
*/
|
||||
abstract public function getCollectionsByActorId(int $owner_id): array;
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
$user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]);
|
||||
return self::collectionsView($request, $user->getId(), $nickname);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function collectionsViewByActorId(Request $request, int $id): array
|
||||
{
|
||||
return self::collectionsView($request, $id, null);
|
||||
@@ -70,7 +95,7 @@ abstract class MetaCollectionController extends FeedController
|
||||
* @param int $id actor id
|
||||
* @param ?string $nickname actor nickname
|
||||
*
|
||||
* @return array twig template options
|
||||
* @return ControllerResultType twig template options
|
||||
*/
|
||||
public function collectionsView(Request $request, int $id, ?string $nickname): array
|
||||
{
|
||||
@@ -113,34 +138,23 @@ abstract class MetaCollectionController extends FeedController
|
||||
// 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
|
||||
$fn = new class($id, $nickname, $request, $this, static::SLUG) {
|
||||
private $id;
|
||||
private $nick;
|
||||
private $request;
|
||||
private $parent;
|
||||
private $slug;
|
||||
|
||||
public function __construct($id, $nickname, $request, $parent, $slug)
|
||||
public function __construct(private int $id, private string $nickname, private Request $request, private object $parent, private string $slug)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->nick = $nickname;
|
||||
$this->request = $request;
|
||||
$this->parent = $parent;
|
||||
$this->slug = $slug;
|
||||
}
|
||||
// there's already an injected function called path,
|
||||
// that maps to Router::url(name, args), but since
|
||||
// I want to preserve nicknames, I think it's better
|
||||
// to use that getUrl function
|
||||
public function getUrl($cid)
|
||||
public function getUrl(int $cid): string
|
||||
{
|
||||
return $this->parent->getCollectionUrl($this->id, $this->nick, $cid);
|
||||
return $this->parent->getCollectionUrl($this->id, $this->nickname, $cid);
|
||||
}
|
||||
// There are many collections in this page and we need two
|
||||
// forms for each one of them: one form to edit the collection's
|
||||
// name and another to remove the collection.
|
||||
|
||||
// creating the edit form
|
||||
public function editForm($collection)
|
||||
public function editForm(object $collection): FormView
|
||||
{
|
||||
$edit = Form::create([
|
||||
['name', TextType::class, [
|
||||
@@ -159,7 +173,7 @@ abstract class MetaCollectionController extends FeedController
|
||||
]);
|
||||
$edit->handleRequest($this->request);
|
||||
if ($edit->isSubmitted() && $edit->isValid()) {
|
||||
$this->parent->setCollectionName($this->id, $this->nick, $collection, $edit->getData()['name']);
|
||||
$this->parent->setCollectionName($this->id, $this->nickname, $collection, $edit->getData()['name']);
|
||||
DB::flush();
|
||||
throw new RedirectException();
|
||||
}
|
||||
@@ -167,7 +181,7 @@ abstract class MetaCollectionController extends FeedController
|
||||
}
|
||||
|
||||
// creating the remove form
|
||||
public function rmForm($collection)
|
||||
public function rmForm(object $collection): FormView
|
||||
{
|
||||
$rm = Form::create([
|
||||
['remove_' . $collection->getId(), SubmitType::class, [
|
||||
@@ -180,7 +194,7 @@ abstract class MetaCollectionController extends FeedController
|
||||
]);
|
||||
$rm->handleRequest($this->request);
|
||||
if ($rm->isSubmitted()) {
|
||||
$this->parent->removeCollection($this->id, $this->nick, $collection);
|
||||
$this->parent->removeCollection($this->id, $this->nickname, $collection);
|
||||
DB::flush();
|
||||
throw new RedirectException();
|
||||
}
|
||||
@@ -198,12 +212,18 @@ abstract class MetaCollectionController extends FeedController
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function collectionsEntryViewNotesByNickname(Request $request, string $nickname, int $cid): array
|
||||
{
|
||||
$user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]);
|
||||
return self::collectionsEntryViewNotesByActorId($request, $user->getId(), $cid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function collectionsEntryViewNotesByActorId(Request $request, int $id, int $cid): array
|
||||
{
|
||||
$collection = $this->getCollectionBy($id, $cid);
|
||||
|
@@ -4,6 +4,11 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Collection\Util\Controller;
|
||||
|
||||
class OrderedCollection extends Collection
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends Collection<T>
|
||||
*/
|
||||
abstract class OrderedCollection extends Collection
|
||||
{
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Collection\Util;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
@@ -39,11 +39,15 @@ use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Formatting;
|
||||
use EventResult;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* */
|
||||
trait MetaCollectionTrait
|
||||
{
|
||||
//protected const SLUG = 'collection';
|
||||
@@ -53,39 +57,43 @@ trait MetaCollectionTrait
|
||||
* create a collection owned by Actor $owner.
|
||||
*
|
||||
* @param Actor $owner The collection's owner
|
||||
* @param array $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param string $name Collection's name
|
||||
*/
|
||||
abstract protected function createCollection(Actor $owner, array $vars, string $name);
|
||||
abstract protected function createCollection(Actor $owner, array $vars, string $name): void;
|
||||
/**
|
||||
* remove item from collections.
|
||||
*
|
||||
* @param Actor $owner Current user
|
||||
* @param array $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param array $items Array of collections's ids to remove the current item from
|
||||
* @param array $collections List of ids of collections owned by $owner
|
||||
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param int[] $items Array of collections's ids to remove the current item from
|
||||
* @param int[] $collections List of ids of collections owned by $owner
|
||||
*/
|
||||
abstract protected function removeItem(Actor $owner, array $vars, array $items, array $collections);
|
||||
abstract protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool;
|
||||
/**
|
||||
* add item to collections.
|
||||
*
|
||||
* @param Actor $owner Current user
|
||||
* @param array $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param array $items Array of collections's ids to add the current item to
|
||||
* @param array $collections List of ids of collections owned by $owner
|
||||
* @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param int[] $items Array of collections's ids to add the current item to
|
||||
* @param int[] $collections List of ids of collections owned by $owner
|
||||
*/
|
||||
abstract protected function addItem(Actor $owner, array $vars, array $items, array $collections);
|
||||
abstract protected function addItem(Actor $owner, array $vars, array $items, array $collections): void;
|
||||
|
||||
/**
|
||||
* Check the route to determine whether the widget should be added
|
||||
*
|
||||
* @param array<string, mixed> $vars
|
||||
*/
|
||||
abstract protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool;
|
||||
abstract protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool;
|
||||
/**
|
||||
* Get array of collections's owned by $actor
|
||||
*
|
||||
* @param Actor $owner Collection's owner
|
||||
* @param ?array $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
|
||||
* @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;
|
||||
|
||||
@@ -93,8 +101,11 @@ trait MetaCollectionTrait
|
||||
* Append Collections widget to the right panel.
|
||||
* It's compose of two forms: one to select collections to add
|
||||
* the current item to, and another to create a new collection.
|
||||
*
|
||||
* @param array<string, mixed> $vars
|
||||
* @param string[] $res
|
||||
*/
|
||||
public function onAppendRightPanelBlock(Request $request, $vars, &$res): bool
|
||||
public function onAppendRightPanelBlock(Request $request, array $vars, array &$res): EventResult
|
||||
{
|
||||
$user = Common::actor();
|
||||
if (\is_null($user)) {
|
||||
@@ -186,7 +197,10 @@ trait MetaCollectionTrait
|
||||
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/pages.css';
|
||||
|
@@ -32,6 +32,9 @@ abstract class Parser
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
|
@@ -28,11 +28,11 @@ declare(strict_types = 1);
|
||||
namespace Component\Conversation\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
@@ -46,6 +46,9 @@ use Component\Conversation\Entity\ConversationMute;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @extends FeedController<\App\Entity\Note>
|
||||
*/
|
||||
class Conversation extends FeedController
|
||||
{
|
||||
/**
|
||||
@@ -55,7 +58,10 @@ class Conversation extends FeedController
|
||||
*
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
*
|
||||
* @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)
|
||||
* @return ControllerResultType 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)
|
||||
*/
|
||||
public function showConversation(Request $request, int $conversation_id): array
|
||||
{
|
||||
@@ -83,7 +89,7 @@ class Conversation extends FeedController
|
||||
* @throws NoSuchNoteException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function addReply(Request $request)
|
||||
{
|
||||
@@ -103,7 +109,7 @@ class Conversation extends FeedController
|
||||
* @throws \App\Util\Exception\RedirectException
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
*
|
||||
* @return array Array containing templating where the form is to be rendered, and the form itself
|
||||
* @return ControllerResultType Array containing templating where the form is to be rendered, and the form itself
|
||||
*/
|
||||
public function muteConversation(Request $request, int $conversation_id)
|
||||
{
|
||||
|
@@ -28,12 +28,11 @@ declare(strict_types = 1);
|
||||
namespace Component\Conversation;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
@@ -41,12 +40,13 @@ use App\Util\Common;
|
||||
use App\Util\Formatting;
|
||||
use Component\Conversation\Entity\Conversation as ConversationEntity;
|
||||
use Component\Conversation\Entity\ConversationMute;
|
||||
use EventResult;
|
||||
use Functional as F;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Conversation extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('conversation', '/conversation/{conversation_id<\d+>}', [Controller\Conversation::class, 'showConversation']);
|
||||
$r->connect('conversation_mute', '/conversation/{conversation_id<\d+>}/mute', [Controller\Conversation::class, 'muteConversation']);
|
||||
@@ -96,16 +96,16 @@ class Conversation extends Component
|
||||
* action, if a user is logged in.
|
||||
*
|
||||
* @param \App\Entity\Note $note The Note being rendered
|
||||
* @param array $actions Contains keys 'url' (linking 'conversation_reply_to'
|
||||
* route), 'title' (used as title for aforementioned url),
|
||||
* 'classes' (CSS styling classes used to visually inform the user of action context),
|
||||
* 'id' (HTML markup id used to redirect user to this anchor upon performing the action)
|
||||
* @param array{url: string, title: string, classes: string, id: string} $actions
|
||||
* Contains keys 'url' (linking 'conversation_reply_to' route),
|
||||
* 'title' (used as title for aforementioned url), 'classes' (CSS styling
|
||||
* classes used to visually inform the user of action context), 'id' (HTML
|
||||
* markup id used to redirect user to this anchor upon performing the
|
||||
* action)
|
||||
*
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onAddNoteActions(Request $request, Note $note, array &$actions): bool
|
||||
public function onAddNoteActions(Request $request, Note $note, array &$actions): EventResult
|
||||
{
|
||||
if (\is_null(Common::user())) {
|
||||
return Event::next;
|
||||
@@ -137,37 +137,16 @@ class Conversation extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posting event to add extra information to Component\Posting form data
|
||||
*
|
||||
* @param array $data Transport data to be filled with reply_to_id
|
||||
*
|
||||
* @throws \App\Util\Exception\ClientException
|
||||
* @throws \App\Util\Exception\NoSuchNoteException
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onPostingModifyData(Request $request, Actor $actor, array &$data): bool
|
||||
{
|
||||
$data['reply_to_id'] = $request->get('_route') === 'conversation_reply_to' && $request->query->has('reply_to_id')
|
||||
? $request->query->getInt('reply_to_id')
|
||||
: null;
|
||||
|
||||
if (!\is_null($data['reply_to_id'])) {
|
||||
Note::ensureCanInteract(Note::getById($data['reply_to_id']), $actor);
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append on note information about user actions.
|
||||
*
|
||||
* @param array $vars Contains information related to Note currently being rendered
|
||||
* @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'])
|
||||
*
|
||||
* @return bool EventHook
|
||||
* @param array<string, mixed> $vars Contains information related to Note currently being rendered
|
||||
* @param array{actors: Actor[], action: string} $result
|
||||
*cContains 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'])
|
||||
*/
|
||||
public function onAppendCardNote(array $vars, array &$result): bool
|
||||
public function onAppendCardNote(array $vars, array &$result): EventResult
|
||||
{
|
||||
if (str_contains($vars['request']->getPathInfo(), 'conversation')) {
|
||||
return Event::next;
|
||||
@@ -194,6 +173,22 @@ class Conversation extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
private function getReplyToIdFromRequest(Request $request): ?int
|
||||
{
|
||||
if (!\is_array($request->get('post_note')) || !\array_key_exists('_next', $request->get('post_note'))) {
|
||||
return null;
|
||||
}
|
||||
$next = parse_url($request->get('post_note')['_next']);
|
||||
if (!\array_key_exists('query', $next)) {
|
||||
return null;
|
||||
}
|
||||
parse_str($next['query'], $query);
|
||||
if (!\array_key_exists('reply_to_id', $query)) {
|
||||
return null;
|
||||
}
|
||||
return (int) $query['reply_to_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs **\App\Component\Posting::onAppendRightPostingBlock**, of the **current page context** in which the given
|
||||
* Actor is in. This is valuable when posting within a group route, allowing \App\Component\Posting to create a
|
||||
@@ -201,12 +196,10 @@ class Conversation extends Component
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): bool
|
||||
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): EventResult
|
||||
{
|
||||
$to_note_id = $request->query->get('reply_to_id');
|
||||
$to_note_id = $this->getReplyToIdFromRequest($request);
|
||||
if (!\is_null($to_note_id)) {
|
||||
// Getting the actor itself
|
||||
$context_actor = Actor::getById(Note::getById((int) $to_note_id)->getActorId());
|
||||
@@ -216,9 +209,30 @@ class Conversation extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minimal Note card to RightPanel template
|
||||
* 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
|
||||
*
|
||||
* @throws \App\Util\Exception\ClientException
|
||||
* @throws \App\Util\Exception\NoSuchNoteException
|
||||
*/
|
||||
public function onPrependPostingForm(Request $request, array &$elements): bool
|
||||
public function onPostingModifyData(Request $request, Actor $actor, array &$data): EventResult
|
||||
{
|
||||
$to_note_id = $this->getReplyToIdFromRequest($request);
|
||||
if (!\is_null($to_note_id)) {
|
||||
Note::ensureCanInteract(Note::getById($to_note_id), $actor);
|
||||
$data['reply_to_id'] = $to_note_id;
|
||||
}
|
||||
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minimal Note card to RightPanel template
|
||||
*
|
||||
* @param string[] $elements
|
||||
*/
|
||||
public function onPrependPostingForm(Request $request, array &$elements): EventResult
|
||||
{
|
||||
$elements[] = Formatting::twigRenderFile('cards/blocks/note_compact_wrapper.html.twig', ['note' => Note::getById((int) $request->query->get('reply_to_id'))]);
|
||||
return Event::next;
|
||||
@@ -230,10 +244,8 @@ class Conversation extends Component
|
||||
*
|
||||
* @param \App\Entity\Note $note Note being deleted
|
||||
* @param \App\Entity\Actor $actor Actor that performed the delete action
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
|
||||
{
|
||||
// Ensure we have the most up to date replies
|
||||
Cache::delete(Note::cacheKeys($note->getId())['replies']);
|
||||
@@ -246,13 +258,13 @@ class Conversation extends Component
|
||||
* Adds extra actions related to Conversation Component, that act upon/from the given Note.
|
||||
*
|
||||
* @param \App\Entity\Note $note Current Note being rendered
|
||||
* @param array $actions Containing 'url' (Controller connected route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if needed)
|
||||
* @param array{url: string, title: string, classes?: string} $actions Containing 'url' (Controller connected
|
||||
* route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if
|
||||
* needed)
|
||||
*
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): bool
|
||||
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): EventResult
|
||||
{
|
||||
if (\is_null($user = Common::user())) {
|
||||
return Event::next;
|
||||
@@ -285,10 +297,8 @@ class Conversation extends Component
|
||||
* Prevents new Notifications to appear for muted conversations
|
||||
*
|
||||
* @param Activity $activity Notification Activity
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onNewNotificationShould(Activity $activity, Actor $actor): bool
|
||||
public function onNewNotificationShould(Activity $activity, Actor $actor): EventResult
|
||||
{
|
||||
if ($activity->getObjectType() === 'note' && ConversationMute::isMuted($activity, $actor)) {
|
||||
return Event::stop;
|
||||
|
@@ -23,9 +23,9 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Conversation\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
|
||||
/**
|
||||
* Entity class for Conversations
|
||||
|
@@ -24,7 +24,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Conversation\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
|
@@ -41,10 +41,15 @@ use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @extends FeedController<\App\Entity\Note>
|
||||
*/
|
||||
class Feeds extends FeedController
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
@@ -60,6 +65,8 @@ class Feeds extends FeedController
|
||||
|
||||
/**
|
||||
* The Home feed represents everything that concerns a certain actor (its subscriptions)
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function home(Request $request): array
|
||||
{
|
||||
|
@@ -25,12 +25,13 @@ namespace Component\Feed;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router;
|
||||
use Component\Feed\Controller as C;
|
||||
use EventResult;
|
||||
|
||||
class Feed extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('feed_public', '/feed/public', [C\Feeds::class, 'public']);
|
||||
$r->connect('feed_home', '/feed/home', [C\Feeds::class, 'home']);
|
||||
|
@@ -23,7 +23,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Feed\tests\Controller;
|
||||
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use Component\Feed\Controller\Feeds;
|
||||
use Jchook\AssertThrows\AssertThrows;
|
||||
|
@@ -34,7 +34,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\FreeNetwork\Controller;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Util\Common;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
|
@@ -32,7 +32,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\FreeNetwork\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Entity\Actor;
|
||||
use Component\FreeNetwork\Util\Discovery;
|
||||
|
@@ -21,15 +21,14 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\FreeNetwork;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\HTTPClient;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
@@ -55,6 +54,7 @@ use Component\FreeNetwork\Util\WebfingerResource;
|
||||
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor;
|
||||
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote;
|
||||
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||
use EventResult;
|
||||
use Exception;
|
||||
use const PREG_SET_ORDER;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
@@ -80,13 +80,13 @@ class FreeNetwork extends Component
|
||||
public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize';
|
||||
private static array $protocols = [];
|
||||
|
||||
public function onInitializeComponent(): bool
|
||||
public function onInitializeComponent(): EventResult
|
||||
{
|
||||
Event::handle('AddFreeNetworkProtocol', [&self::$protocols]);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onAddRoute(RouteLoader $m): bool
|
||||
public function onAddRoute(Router $m): EventResult
|
||||
{
|
||||
// Feeds
|
||||
$m->connect('feed_network', '/feed/network', [Feeds::class, 'network']);
|
||||
@@ -112,7 +112,7 @@ class FreeNetwork extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering)
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
|
||||
{
|
||||
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++]));
|
||||
@@ -120,7 +120,7 @@ class FreeNetwork extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onStartGetProfileAcctUri(Actor $profile, &$acct): bool
|
||||
public function onStartGetProfileAcctUri(Actor $profile, &$acct): EventResult
|
||||
{
|
||||
$wfr = new WebFingerResourceActor($profile);
|
||||
try {
|
||||
@@ -148,7 +148,7 @@ class FreeNetwork extends Component
|
||||
* @throws NoSuchActorException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onEndGetWebFingerResource(string $resource, ?WebfingerResource &$target = null, array $args = []): bool
|
||||
public function onEndGetWebFingerResource(string $resource, ?WebfingerResource &$target = null, array $args = []): EventResult
|
||||
{
|
||||
// * Either we didn't find the profile, then we want to make
|
||||
// the $profile variable null for clarity.
|
||||
@@ -224,7 +224,7 @@ class FreeNetwork extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onStartHostMetaLinks(array &$links): bool
|
||||
public function onStartHostMetaLinks(array &$links): EventResult
|
||||
{
|
||||
foreach (Discovery::supportedMimeTypes() as $type) {
|
||||
$links[] = new XML_XRD_Element_Link(
|
||||
@@ -244,8 +244,10 @@ class FreeNetwork extends Component
|
||||
|
||||
/**
|
||||
* Add a link header for LRDD Discovery
|
||||
*
|
||||
* @param mixed $action
|
||||
*/
|
||||
public function onStartShowHTML($action): bool
|
||||
public function onStartShowHTML($action): EventResult
|
||||
{
|
||||
if ($action instanceof ShowstreamAction) {
|
||||
$resource = $action->getTarget()->getUri();
|
||||
@@ -258,13 +260,13 @@ class FreeNetwork extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onStartDiscoveryMethodRegistration(Discovery $disco): bool
|
||||
public function onStartDiscoveryMethodRegistration(Discovery $disco): EventResult
|
||||
{
|
||||
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodWebfinger');
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onEndDiscoveryMethodRegistration(Discovery $disco): bool
|
||||
public function onEndDiscoveryMethodRegistration(Discovery $disco): EventResult
|
||||
{
|
||||
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodHostMeta');
|
||||
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodLinkHeader');
|
||||
@@ -276,7 +278,7 @@ class FreeNetwork extends Component
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?Response &$response = null): bool
|
||||
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?Response &$response = null): EventResult
|
||||
{
|
||||
if (!\in_array($route, ['freenetwork_hostmeta', 'freenetwork_hostmeta_format', 'freenetwork_webfinger', 'freenetwork_webfinger_format', 'freenetwork_ownerxrd'])) {
|
||||
return Event::next;
|
||||
@@ -344,6 +346,7 @@ class FreeNetwork extends Component
|
||||
* @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
|
||||
*
|
||||
* @example.com/mublog/user
|
||||
*/
|
||||
public static function extractUrlMentions(string $text, string $preMention = '@'): array
|
||||
@@ -375,9 +378,10 @@ class FreeNetwork extends Component
|
||||
* @param $mentions
|
||||
*
|
||||
* @return bool hook return value
|
||||
*
|
||||
* @example.com/mublog/user
|
||||
*/
|
||||
public function onEndFindMentions(Actor $sender, string $text, array &$mentions): bool
|
||||
public function onEndFindMentions(Actor $sender, string $text, array &$mentions): EventResult
|
||||
{
|
||||
$matches = [];
|
||||
|
||||
@@ -496,6 +500,9 @@ class FreeNetwork extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Actor[] $targets
|
||||
*/
|
||||
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
|
||||
{
|
||||
foreach (self::$protocols as $protocol) {
|
||||
@@ -517,8 +524,11 @@ class FreeNetwork extends Component
|
||||
/**
|
||||
* Add fediverse: query expression
|
||||
* // 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): bool
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
|
||||
{
|
||||
if (Formatting::startsWith($term, ['fediverse:'])) {
|
||||
foreach (self::$protocols as $protocol) {
|
||||
@@ -531,7 +541,7 @@ class FreeNetwork extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onPluginVersion(array &$versions): bool
|
||||
public function onPluginVersion(array &$versions): EventResult
|
||||
{
|
||||
$versions[] = [
|
||||
'name' => 'WebFinger',
|
||||
|
@@ -6,7 +6,7 @@ namespace Component\FreeNetwork\Util\WebfingerResource;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use Component\FreeNetwork\Exception\WebfingerReconstructionException;
|
||||
|
@@ -26,7 +26,7 @@ namespace Component\Group\Controller;
|
||||
use App\Core\ActorLocalRoles;
|
||||
use App\Core\Cache;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
@@ -61,6 +61,8 @@ class Group extends Controller
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function groupCreate(Request $request): array
|
||||
{
|
||||
@@ -89,6 +91,8 @@ class Group extends Controller
|
||||
* @throws NicknameTooLongException
|
||||
* @throws NotFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function groupSettings(Request $request, int $id): array
|
||||
{
|
||||
|
@@ -24,10 +24,10 @@ declare(strict_types = 1);
|
||||
namespace Component\Group\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity as E;
|
||||
use App\Util\Common;
|
||||
@@ -40,10 +40,15 @@ use Component\Subscription\Entity\ActorSubscription;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @extends FeedController<\App\Entity\Note>
|
||||
*/
|
||||
class GroupFeed extends FeedController
|
||||
{
|
||||
/**
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function groupView(Request $request, Actor $group): array
|
||||
{
|
||||
@@ -80,6 +85,7 @@ class GroupFeed extends FeedController
|
||||
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)
|
||||
)
|
||||
ORDER BY n.created DESC
|
||||
EOF, ['id' => $group->getId()]);
|
||||
|
||||
return [
|
||||
@@ -95,6 +101,8 @@ class GroupFeed extends FeedController
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function groupViewId(Request $request, int $id): array
|
||||
{
|
||||
@@ -118,6 +126,8 @@ class GroupFeed extends FeedController
|
||||
*
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function groupViewNickname(Request $request, string $nickname): array
|
||||
{
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Group\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Exception\NicknameEmptyException;
|
||||
|
@@ -24,8 +24,7 @@ namespace Component\Group;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
@@ -34,11 +33,12 @@ use App\Util\Nickname;
|
||||
use Component\Group\Controller as C;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
use Component\Notification\Notification;
|
||||
use EventResult;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Group extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$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']);
|
||||
@@ -50,13 +50,20 @@ class Group extends Component
|
||||
/**
|
||||
* Enqueues a notification for an Actor (such as person or group) which means
|
||||
* it shows up in their home feed and such.
|
||||
*
|
||||
* @param Actor[] $targets
|
||||
*/
|
||||
public function onNewNotificationWithTargets(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): bool
|
||||
public function onNewNotificationStart(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): EventResult
|
||||
{
|
||||
foreach ($targets as $target) {
|
||||
if ($target->isGroup()) {
|
||||
// The Group announces to its subscribers
|
||||
Notification::notify($target, $activity, $target->getSubscribers(), $reason);
|
||||
Notification::notify(
|
||||
sender: $target,
|
||||
activity: $activity,
|
||||
targets: $target->getSubscribers(),
|
||||
reason: $reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +72,11 @@ class Group extends Component
|
||||
|
||||
/**
|
||||
* 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): bool
|
||||
public function onAppendCardProfile(array $vars, array &$res): EventResult
|
||||
{
|
||||
$actor = Common::actor();
|
||||
$group = $vars['actor'];
|
||||
@@ -75,7 +85,6 @@ class Group extends Component
|
||||
$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' => 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;
|
||||
}
|
||||
@@ -85,19 +94,30 @@ class Group extends Component
|
||||
*/
|
||||
private function getGroupFromContext(Request $request): ?Actor
|
||||
{
|
||||
if (str_starts_with($request->get('_route'), 'group_actor_view_')) {
|
||||
if (!\is_null($id = $request->get('id'))) {
|
||||
return Actor::getById((int) $id);
|
||||
if (\is_array($request->get('post_note')) && \array_key_exists('_next', $request->get('post_note'))) {
|
||||
$next = parse_url($request->get('post_note')['_next']);
|
||||
$match = Router::match($next['path']);
|
||||
$route = $match['_route'];
|
||||
$identifier = $match['id'] ?? $match['nickname'] ?? null;
|
||||
} else {
|
||||
$route = $request->get('_route');
|
||||
$identifier = $request->get('id') ?? $request->get('nickname');
|
||||
}
|
||||
|
||||
if (!\is_null($nickname = $request->get('nickname'))) {
|
||||
return LocalGroup::getActorByNickname($nickname);
|
||||
if (str_starts_with($route, 'group_actor_view_')) {
|
||||
switch ($route) {
|
||||
case 'group_actor_view_nickname':
|
||||
return LocalGroup::getActorByNickname($identifier);
|
||||
case 'group_actor_view_id':
|
||||
return Actor::getById((int) $identifier);
|
||||
}
|
||||
}
|
||||
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);
|
||||
if (!\is_null($group)) {
|
||||
@@ -116,7 +136,7 @@ class Group extends Component
|
||||
*
|
||||
* @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): bool
|
||||
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): EventResult
|
||||
{
|
||||
$ctx = $this->getGroupFromContext($request);
|
||||
if (!\is_null($ctx)) {
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<h1>Settings</h1>
|
||||
<ul>
|
||||
<li>
|
||||
{% 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 = [{'title': 'Personal Info', 'desc': 'Nickname, Homepage, Bio and more.', 'id': 'settings-personal-info', 'form': personal_info_form}] %}
|
||||
{% 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) }}
|
||||
</li>
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Group\tests\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
|
@@ -25,7 +25,7 @@ namespace Component\Language\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Util\Common;
|
||||
@@ -100,6 +100,8 @@ class Language extends Controller
|
||||
* @throws NoLoggedInUser
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function sortLanguages(Request $request): array
|
||||
{
|
||||
|
@@ -24,7 +24,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Language\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
@@ -119,6 +119,9 @@ class ActorLanguage extends Entity
|
||||
) ?: [Language::getByLocale(Common::config('site', 'language'))];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public static function getActorRelatedLanguagesIds(Actor $actor): array
|
||||
{
|
||||
return Cache::getList(
|
||||
|
@@ -24,7 +24,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Language\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity\Actor;
|
||||
@@ -116,7 +116,7 @@ class Language extends Entity
|
||||
return Cache::getHashMapKey(
|
||||
map_key: 'languages-id',
|
||||
key: (string) $id,
|
||||
calculate_map: fn () => F\reindex(DB::dql('select l from language l'), fn (self $l) => (string) $l->getId()),
|
||||
calculate_map: fn () => F\reindex(DB::dql('SELECT l FROM \Component\Language\Entity\Language AS l'), fn (self $l) => (string) $l->getId()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ class Language extends Entity
|
||||
return Cache::getHashMapKey(
|
||||
'languages',
|
||||
$locale,
|
||||
calculate_map: fn () => F\reindex(DB::dql('select l from language l'), fn (self $l) => $l->getLocale()),
|
||||
calculate_map: fn () => F\reindex(DB::dql('SELECT l FROM \Component\Language\Entity\Language AS l'), fn (self $l) => $l->getLocale()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,16 +134,21 @@ class Language extends Entity
|
||||
return self::getById($note->getLanguageId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getLanguageChoices(): array
|
||||
{
|
||||
$langs = Cache::getHashMap(
|
||||
'languages',
|
||||
fn () => F\reindex(DB::dql('select l from language l'), fn (self $l) => $l->getLocale()),
|
||||
fn () => F\reindex(DB::dql('SELECT l FROM \Component\Language\Entity\Language AS l'), fn (self $l) => $l->getLocale()),
|
||||
);
|
||||
|
||||
return array_merge(...F\map(array_values($langs), fn ($l) => $l->toChoiceFormat()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> */
|
||||
public function toChoiceFormat(): array
|
||||
{
|
||||
return [_m($this->getLongDisplay()) => $this->getLocale()];
|
||||
@@ -152,6 +157,8 @@ class Language extends Entity
|
||||
/**
|
||||
* Get all the available languages as well as the languages $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
|
||||
{
|
||||
|
@@ -23,7 +23,7 @@ namespace Component\Language;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Formatting;
|
||||
@@ -33,18 +33,22 @@ use Component\Language\Entity\ActorLanguage;
|
||||
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use EventResult;
|
||||
use Functional as F;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Language extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('settings_sort_languages', '/settings/sort_languages', [C\Language::class, 'sortLanguages']);
|
||||
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)) {
|
||||
return Event::next;
|
||||
@@ -59,8 +63,11 @@ class Language extends Component
|
||||
|
||||
/**
|
||||
* 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): bool
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
|
||||
{
|
||||
$search_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;
|
||||
|
||||
@@ -103,7 +110,7 @@ class Language extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
|
||||
{
|
||||
$note_aliases = $note_qb->getAllAliases();
|
||||
if (!\in_array('note_language', $note_aliases)) {
|
||||
@@ -116,7 +123,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');
|
||||
}
|
||||
|
||||
$actor_aliases = $note_qb->getAllAliases();
|
||||
$actor_aliases = $actor_qb->getAllAliases();
|
||||
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');
|
||||
}
|
||||
|
@@ -25,10 +25,10 @@ namespace Component\LeftPanel\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Feed;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
@@ -104,7 +104,6 @@ class EditFeeds extends Controller
|
||||
$feed->setUrl($fd[$md5 . '-url']);
|
||||
$feed->setOrdering($fd[$md5 . '-order']);
|
||||
$feed->setTitle($fd[$md5 . '-title']);
|
||||
DB::merge($feed);
|
||||
}
|
||||
DB::flush();
|
||||
Cache::delete($key);
|
||||
@@ -119,7 +118,6 @@ class EditFeeds extends Controller
|
||||
/** @var SubmitButton $remove_button */
|
||||
$remove_button = $form->get($remove_id);
|
||||
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::flush();
|
||||
Cache::delete($key);
|
||||
|
@@ -22,32 +22,34 @@ declare(strict_types = 1);
|
||||
namespace Component\LeftPanel;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Feed;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use Component\LeftPanel\Controller as C;
|
||||
use EventResult;
|
||||
|
||||
class LeftPanel extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('edit_feeds', '/edit-feeds', C\EditFeeds::class);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $route_params
|
||||
*
|
||||
* @throws \App\Util\Exception\DuplicateFoundException
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
* @throws ClientException
|
||||
*/
|
||||
public function onAppendFeed(Actor $actor, string $title, string $route, array $route_params): bool
|
||||
public function onAppendFeed(Actor $actor, string $title, string $route, array $route_params): EventResult
|
||||
{
|
||||
$cache_key = Feed::cacheKey($actor);
|
||||
$feeds = Feed::getFeeds($actor);
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Link\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
|
@@ -21,7 +21,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Link\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use DateTimeInterface;
|
||||
@@ -85,15 +85,15 @@ class NoteToLink extends Entity
|
||||
* properties of $obj with the associative array $args. Doesn't
|
||||
* persist the result
|
||||
*
|
||||
* @param null|mixed $obj
|
||||
* @param (array{link_id: int, note_id: int} & array<string, mixed>) $args
|
||||
*/
|
||||
public static function create(array $args, $obj = null)
|
||||
public static function create(array $args, bool $_delegated_call = false): static
|
||||
{
|
||||
$link = DB::find('link', ['id' => $args['link_id']]);
|
||||
$note = DB::find('note', ['id' => $args['note_id']]);
|
||||
Event::handle('NewLinkFromNote', [$link, $note]);
|
||||
$obj = new self();
|
||||
return parent::create($args, $obj);
|
||||
return parent::createOrUpdate(obj: $obj, args: $args);
|
||||
}
|
||||
|
||||
public static function removeWhereNoteId(int $note_id): mixed
|
||||
|
@@ -23,7 +23,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Link;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Entity\Actor;
|
||||
@@ -31,14 +31,33 @@ use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\HTML;
|
||||
use Component\Link\Entity\NoteToLink;
|
||||
use EventResult;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class Link extends Component
|
||||
{
|
||||
/**
|
||||
* Extract URLs from $content and create the appropriate Link and NoteToLink entities
|
||||
* Note that this persists both a Link and a NoteToLink
|
||||
*
|
||||
* @return array{ link: ?Entity\Link, note_to_link: ?NoteToLink }
|
||||
*/
|
||||
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): bool
|
||||
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
|
||||
*
|
||||
* @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
|
||||
{
|
||||
$ignore = $process_note_content_extra_args['ignoreLinks'] ?? [];
|
||||
if (Common::config('attachments', 'process_links')) {
|
||||
@@ -49,18 +68,13 @@ class Link extends Component
|
||||
if (\in_array($match, $ignore)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$link_id = Entity\Link::getOrCreate($match)->getId();
|
||||
DB::persist(NoteToLink::create(['link_id' => $link_id, 'note_id' => $note->getId()]));
|
||||
} catch (InvalidArgumentException) {
|
||||
continue;
|
||||
}
|
||||
self::maybeCreateLink($match, $note->getId());
|
||||
}
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onRenderPlainTextNoteContent(string &$text): bool
|
||||
public function onRenderPlainTextNoteContent(string &$text): EventResult
|
||||
{
|
||||
$text = $this->replaceURLs($text);
|
||||
return Event::next;
|
||||
@@ -138,7 +152,12 @@ class Link extends Component
|
||||
public const URL_SCHEME_NO_DOMAIN = 4;
|
||||
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
|
||||
$schemes = [
|
||||
@@ -185,6 +204,7 @@ class Link extends Component
|
||||
* Intermediate callback for `replaceURLs()`, which helps resolve some
|
||||
* ambiguous link forms before passing on to the final callback.
|
||||
*
|
||||
* @param string[] $matches
|
||||
* @param callable(string $text): string $callback: return replacement text
|
||||
*/
|
||||
private function callbackHelper(array $matches, callable $callback): string
|
||||
@@ -264,7 +284,7 @@ class Link extends Component
|
||||
return HTML::html(['a' => ['attrs' => $attrs, $url]], options: ['indent' => false]);
|
||||
}
|
||||
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
|
||||
{
|
||||
DB::wrapInTransaction(fn () => NoteToLink::removeWhereNoteId($note->getId()));
|
||||
return Event::next;
|
||||
|
@@ -35,7 +35,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Notification\Controller;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Util\Common;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -44,6 +44,8 @@ class Feed extends Controller
|
||||
{
|
||||
/**
|
||||
* Everything with attention to current user
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function notifications(Request $request): array
|
||||
{
|
||||
@@ -53,9 +55,9 @@ class Feed extends Controller
|
||||
WHERE n.id IN (
|
||||
SELECT act.object_id FROM \App\Entity\Activity AS act
|
||||
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 = :target_id)
|
||||
)
|
||||
EOF, ['id' => $user->getId()]);
|
||||
EOF, [':target_id' => $user->getId()]);
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'page_title' => _m('Notifications'),
|
||||
|
@@ -24,31 +24,51 @@ namespace Component\Notification\Entity;
|
||||
use App\Core\Entity;
|
||||
|
||||
/**
|
||||
* Entity for note attentions
|
||||
* Entity for object 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
|
||||
* @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>
|
||||
* @copyright 2022 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @copyright 2021-2022 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Attention extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $note_id;
|
||||
private string $object_type;
|
||||
private int $object_id;
|
||||
private int $target_id;
|
||||
|
||||
public function setNoteId(int $note_id): self
|
||||
public function setObjectType(string $object_type): self
|
||||
{
|
||||
$this->note_id = $note_id;
|
||||
$this->object_type = mb_substr($object_type, 0, 32);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNoteId(): int
|
||||
public function getObjectType(): string
|
||||
{
|
||||
return $this->note_id;
|
||||
return $this->object_type;
|
||||
}
|
||||
|
||||
public function setObjectId(int $object_id): self
|
||||
{
|
||||
$this->object_id = $object_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getObjectId(): int
|
||||
{
|
||||
return $this->object_id;
|
||||
}
|
||||
|
||||
public function setTargetId(int $target_id): self
|
||||
@@ -68,15 +88,16 @@ class Attention extends Entity
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'note_attention',
|
||||
'description' => 'Note attentions to actors (that are not a mention)',
|
||||
'name' => 'attention',
|
||||
'description' => 'Attentions to actors (these are not mentions)',
|
||||
'fields' => [
|
||||
'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'note_id to give attention'],
|
||||
'object_type' => ['type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'the name of the table this object refers to'],
|
||||
'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'],
|
||||
],
|
||||
'primary key' => ['note_id', 'target_id'],
|
||||
'primary key' => ['object_type', 'object_id', 'target_id'],
|
||||
'indexes' => [
|
||||
'attention_note_id_idx' => ['note_id'],
|
||||
'attention_object_id_idx' => ['object_id'],
|
||||
'attention_target_id_idx' => ['target_id'],
|
||||
],
|
||||
];
|
||||
|
@@ -21,24 +21,24 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Notification\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for attentions
|
||||
* Entity for Notifications
|
||||
*
|
||||
* 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
|
||||
* @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 Hugo Sales <hugo@hsal.es>
|
||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @author Diogo Peralta Cordeiro <@diogo.site>
|
||||
* @copyright 2021-2022 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Notification extends Entity
|
||||
@@ -117,7 +117,7 @@ class Notification extends Entity
|
||||
/**
|
||||
* Pull the complete list of known activity context notifications for this activity.
|
||||
*
|
||||
* @return array of integer actor ids (also group profiles)
|
||||
* @return int[] actor ids (also group profiles)
|
||||
*/
|
||||
public static function getNotificationTargetIdsByActivity(int|Activity $activity_id): array
|
||||
{
|
||||
@@ -129,11 +129,17 @@ class Notification extends Entity
|
||||
return $targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getNotificationTargetsByActivity(int|Activity $activity_id): array
|
||||
{
|
||||
return DB::findBy(Actor::class, ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public static function getAllActivitiesTargetedAtActor(Actor $actor): array
|
||||
{
|
||||
return DB::dql(<<<'EOF'
|
||||
|
@@ -21,26 +21,26 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Notification;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Queue\Queue;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Queue;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\FreeNetwork\FreeNetwork;
|
||||
use Component\Notification\Controller\Feed;
|
||||
use EventResult;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class Notification extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $m): bool
|
||||
public function onAddRoute(Router $m): EventResult
|
||||
{
|
||||
$m->connect('feed_notifications', '/feed/notifications', [Feed::class, 'notifications']);
|
||||
return Event::next;
|
||||
@@ -49,7 +49,7 @@ class Notification extends Component
|
||||
/**
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): bool
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
|
||||
{
|
||||
DB::persist(\App\Entity\Feed::create([
|
||||
'actor_id' => $actor_id,
|
||||
@@ -64,24 +64,59 @@ class Notification extends Component
|
||||
/**
|
||||
* Enqueues a notification for an Actor (such as person or group) which means
|
||||
* 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 $ids_already_known = [], ?string $reason = null): bool
|
||||
public function onNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): EventResult
|
||||
{
|
||||
$targets = $activity->getNotificationTargets(ids_already_known: $ids_already_known, sender_id: $sender->getId());
|
||||
if (Event::handle('NewNotificationWithTargets', [$sender, $activity, $targets, $reason]) === Event::next) {
|
||||
self::notify($sender, $activity, $targets, $reason);
|
||||
// Ensure targets are all actor objects and unique
|
||||
$effective_targets = [];
|
||||
foreach ($targets as $target) {
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
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)) {
|
||||
return Event::stop;
|
||||
@@ -91,17 +126,20 @@ class Notification extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring given Activity to Targets's attention
|
||||
* Bring given Activity to Targets' knowledge.
|
||||
* This will flush a Notification to DB.
|
||||
*
|
||||
* @return true if successful, false otherwise
|
||||
* @param Actor[] $targets
|
||||
*
|
||||
* @return bool true if successful, false otherwise
|
||||
*/
|
||||
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
|
||||
{
|
||||
$remote_targets = [];
|
||||
foreach ($targets as $target) {
|
||||
if ($target->getIsLocal()) {
|
||||
if ($target->hasBlocked($activity->getActor())) {
|
||||
Log::info("Not saving reply to actor {$target->getId()} from sender {$sender->getId()} because of a block.");
|
||||
if ($target->hasBlocked($author = $activity->getActor())) {
|
||||
Log::info("Not saving notification to actor {$target->getId()} from sender {$sender->getId()} because receiver blocked author {$author->getId()}.");
|
||||
continue;
|
||||
}
|
||||
if (Event::handle('NewNotificationShould', [$activity, $target]) === Event::next) {
|
||||
@@ -113,7 +151,7 @@ class Notification extends Component
|
||||
}
|
||||
Queue::enqueue(
|
||||
payload: [$sender, $activity, $target, $reason],
|
||||
queue: 'notification_local',
|
||||
queue: 'NotificationLocal',
|
||||
priority: true,
|
||||
);
|
||||
} else {
|
||||
@@ -124,7 +162,7 @@ class Notification extends Component
|
||||
}
|
||||
// XXX: Unideal as in failures the rollback will leave behind a false notification,
|
||||
// but most notifications (all) require flushing the objects first
|
||||
// Should be okay as long as implementors bear this in mind
|
||||
// Should be okay as long as implementations bear this in mind
|
||||
try {
|
||||
DB::wrapInTransaction(fn () => DB::persist(Entity\Notification::create([
|
||||
'activity_id' => $activity->getId(),
|
||||
@@ -132,7 +170,7 @@ class Notification extends Component
|
||||
'reason' => $reason,
|
||||
])));
|
||||
} catch (Exception|Throwable $e) {
|
||||
// We do our best not to record duplicated notifications, but it's not insane that can happen
|
||||
// We do our best not to record duplicate notifications, but it's not insane that can happen
|
||||
Log::error('It was attempted to record an invalid notification!', [$e]);
|
||||
}
|
||||
}
|
||||
@@ -140,7 +178,7 @@ class Notification extends Component
|
||||
if ($remote_targets !== []) {
|
||||
Queue::enqueue(
|
||||
payload: [$sender, $activity, $remote_targets, $reason],
|
||||
queue: 'notification_remote',
|
||||
queue: 'NotificationRemote',
|
||||
priority: false,
|
||||
);
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Person\Controller;
|
||||
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity as E;
|
||||
use App\Entity\LocalUser;
|
||||
@@ -34,11 +34,16 @@ use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @extends FeedController<\App\Entity\Note>
|
||||
*/
|
||||
class PersonFeed extends FeedController
|
||||
{
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function personViewId(Request $request, int $id): array
|
||||
{
|
||||
@@ -62,6 +67,8 @@ class PersonFeed extends FeedController
|
||||
*
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function personViewNickname(Request $request, string $nickname): array
|
||||
{
|
||||
@@ -73,6 +80,9 @@ class PersonFeed extends FeedController
|
||||
return $this->personView($request, $person);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function personView(Request $request, Actor $person): array
|
||||
{
|
||||
return [
|
||||
|
@@ -38,11 +38,12 @@ namespace Component\Person\Controller;
|
||||
// {{{ Imports
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\AuthenticationException;
|
||||
use App\Util\Exception\NicknameEmptyException;
|
||||
@@ -87,12 +88,15 @@ class PersonSettings extends Controller
|
||||
* @throws NoLoggedInUser
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function allSettings(Request $request, LanguageController $language): array
|
||||
{
|
||||
// Ensure the user is logged in and retrieve Actor object for given user
|
||||
$user = Common::ensureLoggedIn();
|
||||
$actor = $user->getActor();
|
||||
// Must be persisted
|
||||
$actor = DB::findOneBy(Actor::class, ['id' => $user->getId()]);
|
||||
|
||||
$personal_form = ActorForms::personalInfo(request: $request, scope: $actor, target: $actor);
|
||||
$email_form = self::email($request);
|
||||
@@ -101,7 +105,7 @@ class PersonSettings extends Controller
|
||||
$language_form = $language->settings($request);
|
||||
|
||||
return [
|
||||
'_template' => 'settings/base.html.twig',
|
||||
'_template' => 'person/settings.html.twig',
|
||||
'personal_info_form' => $personal_form->createView(),
|
||||
'email_form' => $email_form->createView(),
|
||||
'password_form' => $password_form->createView(),
|
||||
@@ -203,6 +207,8 @@ class PersonSettings extends Controller
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
* @throws NoLoggedInUser
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType[]
|
||||
*/
|
||||
private static function notifications(Request $request): array
|
||||
{
|
||||
@@ -284,7 +290,7 @@ class PersonSettings extends Controller
|
||||
$data = $form->getData();
|
||||
unset($data['translation_domain']);
|
||||
try {
|
||||
[$entity, $is_update] = UserNotificationPrefs::createOrUpdate(
|
||||
[$entity, $is_update] = UserNotificationPrefs::checkExistingAndCreateOrUpdate(
|
||||
array_merge(['user_id' => $user->getId(), 'transport' => $transport_name], $data),
|
||||
find_by_keys: ['user_id', 'transport'],
|
||||
);
|
||||
|
@@ -23,13 +23,14 @@ namespace Component\Person;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router;
|
||||
use App\Util\Nickname;
|
||||
use Component\Person\Controller as C;
|
||||
use EventResult;
|
||||
|
||||
class Person extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$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]);
|
||||
|
@@ -33,6 +33,10 @@
|
||||
<li>
|
||||
{{ macros.settings_details_container('Notifications', 'Enable/disable notifications (Email, XMPP, Replies...)', 'notifications', tabbed_forms_notify, _context) }}
|
||||
</li>
|
||||
<li>
|
||||
{% set other_tabs = handle_event('PopulateSettingsTabs', app.request, 'api') %}
|
||||
{{ macros.settings_details_container('API', 'API settings', 'settings-other-details', other_tabs, _context) }}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock body %}
|
@@ -23,8 +23,8 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Person\tests\Controller;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\DB;
|
||||
use App\Core\Router;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use Jchook\AssertThrows\AssertThrows;
|
||||
@@ -33,10 +33,6 @@ class PersonSettingsTest extends GNUsocialTestCase
|
||||
{
|
||||
use AssertThrows;
|
||||
|
||||
/**
|
||||
* @covers \App\Controller\PersonSettings::allSettings
|
||||
* @covers \App\Controller\PersonSettings::personalInfo
|
||||
*/
|
||||
public function testPersonalInfo()
|
||||
{
|
||||
$client = static::createClient();
|
||||
@@ -55,19 +51,15 @@ class PersonSettingsTest extends GNUsocialTestCase
|
||||
]);
|
||||
$changed_user = DB::findOneBy(LocalUser::class, ['id' => $user->getId()]);
|
||||
$actor = $changed_user->getActor();
|
||||
static::assertSame($changed_user->getNickname(), 'form_test_user_new_nickname');
|
||||
static::assertSame($actor->getNickname(), 'form_test_user_new_nickname');
|
||||
static::assertSame($actor->getFullName(), 'Form User');
|
||||
static::assertSame($actor->getHomepage(), 'https://gnu.org');
|
||||
static::assertSame($actor->getBio(), 'I was born at a very young age');
|
||||
static::assertSame($actor->getLocation(), 'right here');
|
||||
// static::assertSame($changed_user->getPhoneNumber()->getNationalNumber(), '908555842');
|
||||
static::assertSame('form_test_user_new_nickname', $changed_user->getNickname());
|
||||
static::assertSame('form_test_user_new_nickname', $actor->getNickname());
|
||||
static::assertSame('Form User', $actor->getFullName());
|
||||
static::assertSame('https://gnu.org', $actor->getHomepage());
|
||||
static::assertSame('I was born at a very young age', $actor->getBio());
|
||||
static::assertSame('right here', $actor->getLocation());
|
||||
// static::assertSame('908555842', $changed_user->getPhoneNumber()->getNationalNumber());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \App\Controller\PersonSettings::account
|
||||
* @covers \App\Controller\PersonSettings::allSettings
|
||||
*/
|
||||
public function testEmail()
|
||||
{
|
||||
$client = static::createClient();
|
||||
@@ -86,10 +78,6 @@ class PersonSettingsTest extends GNUsocialTestCase
|
||||
static::assertSame($changed_user->getIncomingEmail(), 'incoming@provider.any');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \App\Controller\PersonSettings::account
|
||||
* @covers \App\Controller\PersonSettings::allSettings
|
||||
*/
|
||||
public function testCorrectPassword()
|
||||
{
|
||||
$client = static::createClient();
|
||||
@@ -108,10 +96,6 @@ class PersonSettingsTest extends GNUsocialTestCase
|
||||
static::assertTrue($changed_user->checkPassword('this is some test password'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \App\Controller\PersonSettings::account
|
||||
* @covers \App\Controller\PersonSettings::allSettings
|
||||
*/
|
||||
public function testAccountWrongPassword()
|
||||
{
|
||||
$client = static::createClient();
|
||||
@@ -130,10 +114,6 @@ class PersonSettingsTest extends GNUsocialTestCase
|
||||
}
|
||||
|
||||
// TODO: First actually implement this functionality
|
||||
// /**
|
||||
// * @covers \App\Controller\PersonSettings::allSettings
|
||||
// * @covers \App\Controller\PersonSettings::notifications
|
||||
// */
|
||||
// public function testNotifications()
|
||||
// {
|
||||
// $client = static::createClient();
|
||||
|
@@ -52,8 +52,8 @@ class Posting extends Controller
|
||||
content_type: $data['content_type'],
|
||||
locale: $data['language'],
|
||||
scope: VisibilityScope::from($data['visibility']),
|
||||
targets: isset($target) ? [$target] : [],
|
||||
reply_to: $data['reply_to_id'],
|
||||
attentions: isset($target) ? [$target] : [],
|
||||
reply_to: \array_key_exists('reply_to_id', $data) ? $data['reply_to_id'] : null,
|
||||
attachments: $data['attachments'],
|
||||
process_note_content_extra_args: $extra_args,
|
||||
);
|
||||
@@ -61,9 +61,9 @@ class Posting extends Controller
|
||||
return Core\Form::forceRedirect($form, $request);
|
||||
}
|
||||
} 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.'));
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ use App\Core\ActorLocalRoles;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
|
@@ -24,16 +24,16 @@ declare(strict_types = 1);
|
||||
namespace Component\Posting;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\BugFoundException;
|
||||
@@ -44,11 +44,13 @@ use App\Util\Exception\ServerException;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\HTML;
|
||||
use Component\Attachment\Entity\ActorToAttachment;
|
||||
use Component\Attachment\Entity\Attachment;
|
||||
use Component\Attachment\Entity\AttachmentToNote;
|
||||
use Component\Conversation\Conversation;
|
||||
use Component\Language\Entity\Language;
|
||||
use Component\Notification\Entity\Attention;
|
||||
use Functional as F;
|
||||
use EventResult;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -56,7 +58,7 @@ class Posting extends Component
|
||||
{
|
||||
public const route = 'posting_form_action';
|
||||
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect(self::route, '/form/posting', Controller\Posting::class);
|
||||
return Event::next;
|
||||
@@ -66,13 +68,15 @@ class Posting extends Component
|
||||
* HTML render event handler responsible for adding and handling
|
||||
* the result of adding the note submission form, only if a user is logged in
|
||||
*
|
||||
* @param array{post_form?: FormInterface} $res
|
||||
*
|
||||
* @throws BugFoundException
|
||||
* @throws ClientException
|
||||
* @throws DuplicateFoundException
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onAddMainRightPanelBlock(Request $request, array &$res): bool
|
||||
public function onAddMainRightPanelBlock(Request $request, array &$res): EventResult
|
||||
{
|
||||
if (\is_null($user = Common::user()) || preg_match('(feed|conversation|group|view)', $request->get('_route')) === 0) {
|
||||
return Event::next;
|
||||
@@ -84,17 +88,33 @@ 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 DuplicateFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
|
||||
*/
|
||||
public static function storeLocalPage(
|
||||
public static function storeLocalArticle(
|
||||
Actor $actor,
|
||||
?string $content,
|
||||
string $content_type,
|
||||
?string $locale = null,
|
||||
?VisibilityScope $scope = null,
|
||||
array $targets = [],
|
||||
array $attentions = [],
|
||||
null|int|Note $reply_to = null,
|
||||
array $attachments = [],
|
||||
array $processed_attachments = [],
|
||||
@@ -104,13 +124,13 @@ class Posting extends Component
|
||||
string $source = 'web',
|
||||
?string $title = null,
|
||||
): array {
|
||||
[$activity, $note, $attention_ids] = self::storeLocalNote(
|
||||
[$activity, $note, $effective_attentions] = self::storeLocalNote(
|
||||
actor: $actor,
|
||||
content: $content,
|
||||
content_type: $content_type,
|
||||
locale: $locale,
|
||||
scope: $scope,
|
||||
targets: $targets,
|
||||
attentions: $attentions,
|
||||
reply_to: $reply_to,
|
||||
attachments: $attachments,
|
||||
processed_attachments: $processed_attachments,
|
||||
@@ -119,16 +139,24 @@ class Posting extends Component
|
||||
rendered: $rendered,
|
||||
source: $source,
|
||||
);
|
||||
$note->setType('page');
|
||||
$note->setType('article');
|
||||
$note->setTitle($title);
|
||||
|
||||
if ($flush_and_notify) {
|
||||
// Flush before notification
|
||||
DB::flush();
|
||||
Event::handle('NewNotification', [$actor, $activity, ['object' => $attention_ids], _m('{nickname} created a page {note_id}.', ['{nickname}' => $actor->getNickname(), '{note_id}' => $activity->getObjectId()])]);
|
||||
Event::handle('NewNotification', [
|
||||
$actor,
|
||||
$activity,
|
||||
$effective_attentions,
|
||||
_m('Actor {actor_id} created article {note_id}.', [
|
||||
'{actor_id}' => $actor->getId(),
|
||||
'{note_id}' => $activity->getObjectId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
return [$activity, $note, $attention_ids];
|
||||
return [$activity, $note, $effective_attentions];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,11 +169,11 @@ class Posting extends Component
|
||||
* @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 array $targets Actor|int[]: In Group/To Person or Bot, registers an attention between note and target
|
||||
* @param Actor[]|int[] $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and targte
|
||||
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
|
||||
* @param array $attachments UploadedFile[] to be stored as GSFiles associated to this note
|
||||
* @param array $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
|
||||
* @param array $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
|
||||
* @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
|
||||
@@ -154,7 +182,7 @@ class Posting extends Component
|
||||
* @throws DuplicateFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array [Activity, Note, int[]] Activity, Note, Attention Ids
|
||||
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
|
||||
*/
|
||||
public static function storeLocalNote(
|
||||
Actor $actor,
|
||||
@@ -162,7 +190,7 @@ class Posting extends Component
|
||||
string $content_type,
|
||||
?string $locale = null,
|
||||
?VisibilityScope $scope = null,
|
||||
array $targets = [],
|
||||
array $attentions = [],
|
||||
null|int|Note $reply_to = null,
|
||||
array $attachments = [],
|
||||
array $processed_attachments = [],
|
||||
@@ -173,6 +201,8 @@ class Posting extends Component
|
||||
): array {
|
||||
$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());
|
||||
|
||||
/** @var array<int, array{ mentioned?: array<int, Actor|LocalUser> }> $mentions */
|
||||
$mentions = [];
|
||||
if (\is_null($rendered) && !empty($content)) {
|
||||
Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $locale, &$mentions]);
|
||||
@@ -209,7 +239,7 @@ class Posting extends Component
|
||||
if (!\is_null($reply_to_id)) {
|
||||
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
|
||||
// list, as that means they need to be refetched, or some would be missed
|
||||
// list, as that means they need to be re-fetched, or some would be missed
|
||||
if (Cache::exists(Note::cacheKeys($reply_to_id)['replies'])) {
|
||||
Cache::listPushRight(Note::cacheKeys($reply_to_id)['replies'], $note);
|
||||
}
|
||||
@@ -221,12 +251,12 @@ class Posting extends Component
|
||||
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 ensured
|
||||
// These are note attachments now, and not just attachments, ensure these relations are respected
|
||||
if ($processed_attachments !== []) {
|
||||
foreach ($processed_attachments as [$a, $fname]) {
|
||||
// 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
|
||||
if (DB::count('actor_to_attachment', $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor->getId()]) === 0) {
|
||||
if (DB::count(ActorToAttachment::class, $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor->getId()]) === 0) {
|
||||
DB::persist(ActorToAttachment::create($args));
|
||||
}
|
||||
DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $note->getId(), 'title' => $fname]));
|
||||
@@ -242,13 +272,38 @@ class Posting extends Component
|
||||
]);
|
||||
DB::persist($activity);
|
||||
|
||||
$attention_ids = [];
|
||||
foreach ($targets as $target) {
|
||||
$target_id = \is_int($target) ? $target : $target->getId();
|
||||
DB::persist(Attention::create(['note_id' => $note->getId(), 'target_id' => $target_id]));
|
||||
$attention_ids[$target_id] = true;
|
||||
$effective_attentions = [];
|
||||
foreach ($attentions as $target) {
|
||||
if (\is_int($target)) {
|
||||
$target_id = $target;
|
||||
$add = !\array_key_exists($target_id, $effective_attentions);
|
||||
$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) {
|
||||
// Flush before notification
|
||||
@@ -256,21 +311,21 @@ class Posting extends Component
|
||||
Event::handle('NewNotification', [
|
||||
$actor,
|
||||
$activity,
|
||||
[
|
||||
'note-attention' => $attention_ids,
|
||||
'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(),
|
||||
$effective_attentions,
|
||||
_m('Actor {actor_id} created note {note_id}.', [
|
||||
'{actor_id}' => $actor->getId(),
|
||||
'{note_id}' => $activity->getObjectId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
return [$activity, $note, $attention_ids];
|
||||
return [$activity, $note, $effective_attentions];
|
||||
}
|
||||
|
||||
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) {
|
||||
case 'text/plain':
|
||||
|
@@ -38,12 +38,17 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @extends FeedController<\App\Entity\Note>
|
||||
*/
|
||||
class Search extends FeedController
|
||||
{
|
||||
/**
|
||||
* Handle a search query
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function handle(Request $request)
|
||||
public function handle(Request $request): array
|
||||
{
|
||||
$actor = Common::actor();
|
||||
$language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null;
|
||||
|
@@ -27,10 +27,12 @@ use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Formatting;
|
||||
use EventResult;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormView;
|
||||
@@ -39,9 +41,10 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Search extends Component
|
||||
{
|
||||
public function onAddRoute($r)
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('search', '/search', Controller\Search::class);
|
||||
return EventResult::next;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,9 +134,11 @@ class Search extends Component
|
||||
/**
|
||||
* Add the search form to the site header
|
||||
*
|
||||
* @param string[] $elements
|
||||
*
|
||||
* @throws RedirectException
|
||||
*/
|
||||
public function onPrependRightPanelBlock(Request $request, array &$elements): bool
|
||||
public function onPrependRightPanelBlock(Request $request, array &$elements): EventResult
|
||||
{
|
||||
$elements[] = Formatting::twigRenderFile('cards/search/view.html.twig', ['search' => self::searchForm($request)]);
|
||||
return Event::next;
|
||||
@@ -142,11 +147,9 @@ class Search extends Component
|
||||
/**
|
||||
* Output our dedicated stylesheet
|
||||
*
|
||||
* @param array $styles stylesheets path
|
||||
*
|
||||
* @return bool hook value; true means continue processing, false means stop
|
||||
* @param string[] $styles stylesheets path
|
||||
*/
|
||||
public function onEndShowStyles(array &$styles, string $route): bool
|
||||
public function onEndShowStyles(array &$styles, string $route): EventResult
|
||||
{
|
||||
$styles[] = 'components/Search/assets/css/view.css';
|
||||
return Event::next;
|
||||
|
@@ -23,11 +23,11 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Subscription\Controller;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
@@ -45,6 +45,8 @@ class Subscribers extends CircleController
|
||||
{
|
||||
/**
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function subscribersByActor(Request $request, Actor $actor): array
|
||||
{
|
||||
@@ -61,6 +63,8 @@ class Subscribers extends CircleController
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function subscribersByActorId(Request $request, int $id): array
|
||||
{
|
||||
@@ -78,6 +82,8 @@ class Subscribers extends CircleController
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
* @throws ClientException
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function subscribersAdd(Request $request, int $object_id): array
|
||||
{
|
||||
@@ -126,6 +132,8 @@ class Subscribers extends CircleController
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
* @throws ClientException
|
||||
* @throws RedirectException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function subscribersRemove(Request $request, int $object_id): array
|
||||
{
|
||||
|
@@ -38,6 +38,8 @@ class Subscriptions extends CircleController
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function subscriptionsByActorId(Request $request, int $id): array
|
||||
{
|
||||
@@ -48,7 +50,10 @@ class Subscriptions extends CircleController
|
||||
return $this->subscriptionsByActor($request, $actor);
|
||||
}
|
||||
|
||||
public function subscriptionsByActor(Request $request, Actor $actor)
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function subscriptionsByActor(Request $request, Actor $actor): array
|
||||
{
|
||||
return [
|
||||
'_template' => 'collection/actors.html.twig',
|
||||
|
@@ -114,27 +114,6 @@ 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
|
||||
{
|
||||
return [
|
||||
|
@@ -24,12 +24,11 @@ declare(strict_types = 1);
|
||||
namespace Component\Subscription;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
@@ -37,14 +36,15 @@ use App\Util\Common;
|
||||
use App\Util\Exception\DuplicateFoundException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\Notification\Entity\Attention;
|
||||
use Component\Subscription\Controller\Subscribers as SubscribersController;
|
||||
use Component\Subscription\Controller\Subscriptions as SubscriptionsController;
|
||||
|
||||
use EventResult;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Subscription extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$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']);
|
||||
@@ -58,6 +58,8 @@ class Subscription extends Component
|
||||
*
|
||||
* @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed
|
||||
* @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
|
||||
{
|
||||
@@ -97,22 +99,24 @@ class Subscription extends Component
|
||||
$subscription = DB::findOneBy(table: Entity\ActorSubscription::class, criteria: $opts, return_null: true);
|
||||
$activity = null;
|
||||
if (\is_null($subscription)) {
|
||||
DB::persist(Entity\ActorSubscription::create($opts));
|
||||
DB::persist($subscription = Entity\ActorSubscription::create($opts));
|
||||
$activity = Activity::create([
|
||||
'actor_id' => $subscriber_id,
|
||||
'verb' => 'subscribe',
|
||||
'object_type' => 'actor',
|
||||
'object_type' => Actor::schemaName(),
|
||||
'object_id' => $subscribed_id,
|
||||
'source' => $source,
|
||||
]);
|
||||
DB::persist($activity);
|
||||
DB::persist(Attention::create(['object_type' => Activity::schemaName(), 'object_id' => $activity->getId(), 'target_id' => $subscribed_id]));
|
||||
|
||||
Event::handle('NewNotification', [
|
||||
\is_int($subject) ? $subject : Actor::getById($subscriber_id),
|
||||
$activity,
|
||||
['object' => [$activity->getObjectId()]],
|
||||
_m('{subject} subscribed to {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $activity->getObjectId()]),
|
||||
[$subscribed_id],
|
||||
$reason = _m('{subject} subscribed to {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $activity->getObjectId()]),
|
||||
]);
|
||||
Event::handle('NewSubscriptionEnd', [$subject, $activity, $object, $reason]);
|
||||
}
|
||||
return $activity;
|
||||
}
|
||||
@@ -146,21 +150,22 @@ class Subscription extends Component
|
||||
if (!\is_null($subscription)) {
|
||||
// Remove Subscription
|
||||
DB::remove($subscription);
|
||||
$previous_follow_activity = DB::findBy('activity', ['verb' => 'subscribe', 'object_type' => 'actor', 'object_id' => $subscribed_id], order_by: ['created' => 'DESC'])[0];
|
||||
$previous_follow_activity = DB::findBy(Activity::class, ['verb' => 'subscribe', 'object_type' => Actor::schemaName(), 'object_id' => $subscribed_id], order_by: ['created' => 'DESC'])[0];
|
||||
// Store Activity
|
||||
$activity = Activity::create([
|
||||
'actor_id' => $subscriber_id,
|
||||
'verb' => 'undo',
|
||||
'object_type' => 'activity',
|
||||
'object_type' => Activity::schemaName(),
|
||||
'object_id' => $previous_follow_activity->getId(),
|
||||
'source' => $source,
|
||||
]);
|
||||
DB::persist($activity);
|
||||
|
||||
DB::persist(Attention::create(['object_type' => Activity::schemaName(), 'object_id' => $activity->getId(), 'target_id' => $subscribed_id]));
|
||||
Event::handle('NewNotification', [
|
||||
\is_int($subject) ? $subject : Actor::getById($subscriber_id),
|
||||
$activity,
|
||||
['object' => [$previous_follow_activity->getObjectId()]],
|
||||
[$subscribed_id],
|
||||
_m('{subject} unsubscribed from {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $previous_follow_activity->getObjectId()]),
|
||||
]);
|
||||
}
|
||||
@@ -175,16 +180,15 @@ class Subscription extends Component
|
||||
* **unsubscribe** a given **Actor**.
|
||||
*
|
||||
* @param Actor $object The Actor on which the action is to be performed
|
||||
* @param array $actions An array containing all actions added to the
|
||||
* @param array<array{url: string, title: string, classes: string, id: string}> $actions
|
||||
* An array containing all actions added to the
|
||||
* current profile, this event adds an action to it
|
||||
*
|
||||
* @throws DuplicateFoundException
|
||||
* @throws NotFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onAddProfileActions(Request $request, Actor $object, array &$actions): bool
|
||||
public function onAddProfileActions(Request $request, Actor $object, array &$actions): EventResult
|
||||
{
|
||||
// Action requires a user to be logged in
|
||||
// We know it's a LocalUser, which has the same id as Actor
|
||||
|
@@ -15,7 +15,12 @@ class Tag extends Controller
|
||||
// TODO: Use Feed::query
|
||||
// TODO: If ?canonical=something, respect
|
||||
// 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();
|
||||
$page = $this->int('page') ?: 1;
|
||||
@@ -46,7 +51,10 @@ class Tag extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
public function single_note_tag(string $tag)
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function single_note_tag(string $tag): array
|
||||
{
|
||||
return $this->process(
|
||||
tag_single_or_multi: $tag,
|
||||
@@ -57,7 +65,10 @@ class Tag extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function multi_note_tags(string $tags)
|
||||
/**
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function multi_note_tags(string $tags): array
|
||||
{
|
||||
return $this->process(
|
||||
tag_single_or_multi: explode(',', $tags),
|
||||
|
@@ -22,9 +22,9 @@ declare(strict_types = 1);
|
||||
namespace Component\Tag\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use Component\Language\Entity\Language;
|
||||
@@ -134,6 +134,9 @@ class NoteTag extends Entity
|
||||
return "note-tags-{$note_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NoteTag[]
|
||||
*/
|
||||
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]));
|
||||
|
@@ -22,7 +22,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Tag\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use Component\Tag\Tag;
|
||||
use DateTimeInterface;
|
||||
@@ -119,6 +119,8 @@ class NoteTagBlock extends Entity
|
||||
/**
|
||||
* Check whether $note_tag is considered blocked by one of
|
||||
* $note_tag_blocks
|
||||
*
|
||||
* @param NoteTagBlock[] $note_tag_blocks
|
||||
*/
|
||||
public static function checkBlocksNoteTag(NoteTag $note_tag, array $note_tag_blocks): bool
|
||||
{
|
||||
|
@@ -24,11 +24,11 @@ declare(strict_types = 1);
|
||||
namespace Component\Tag;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
@@ -42,6 +42,7 @@ use Component\Tag\Entity\NoteTag;
|
||||
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use EventResult;
|
||||
use Functional as F;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -60,7 +61,7 @@ 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_SLUG_REGEX = '[A-Za-z0-9]{1,64}';
|
||||
|
||||
public function onAddRoute($r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$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']);
|
||||
@@ -68,9 +69,62 @@ class Tag extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* Process note by extracting any tags present
|
||||
* @param array{tag_use_canonical?: bool} $extra_args
|
||||
*/
|
||||
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): bool
|
||||
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
|
||||
*
|
||||
* @param array{TagProcessed?: bool} $extra_args
|
||||
*/
|
||||
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): EventResult
|
||||
{
|
||||
if ($extra_args['TagProcessed'] ?? false) {
|
||||
return Event::next;
|
||||
@@ -82,26 +136,12 @@ class Tag extends Component
|
||||
$matched_tags = array_unique(F\map($matched_tags, fn ($m) => $m[2]));
|
||||
foreach ($matched_tags as $match) {
|
||||
$tag = self::extract($match);
|
||||
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);
|
||||
}
|
||||
self::maybeCreateTag(tag: $tag, note_id: $note->getId(), lang_id: $note->getLanguageId());
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onRenderPlainTextNoteContent(string &$text, ?string $locale = null): bool
|
||||
public function onRenderPlainTextNoteContent(string &$text, ?string $locale = null): EventResult
|
||||
{
|
||||
$text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $locale), $text);
|
||||
return Event::next;
|
||||
@@ -178,8 +218,11 @@ class Tag extends Component
|
||||
* 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
|
||||
*
|
||||
* @param mixed $note_expr
|
||||
* @param mixed $actor_expr
|
||||
*/
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
|
||||
{
|
||||
if (!str_contains($term, ':')) {
|
||||
return Event::next;
|
||||
@@ -218,7 +261,7 @@ class Tag extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
|
||||
{
|
||||
if (!\in_array('note_tag', $note_qb->getAllAliases())) {
|
||||
$note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id');
|
||||
@@ -229,13 +272,20 @@ class Tag extends Component
|
||||
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)')]];
|
||||
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'])) {
|
||||
throw new ClientException(_m('Missing Use Canonical preference for Tags.'));
|
||||
|
@@ -75,7 +75,7 @@
|
||||
"friendsofphp/php-cs-fixer": "^3.2.1",
|
||||
"jchook/phpunit-assert-throws": "^1.0",
|
||||
"niels-de-blaauw/php-doc-check": "^0.2.2",
|
||||
"phpstan/phpstan": "dev-master",
|
||||
"phpstan/phpstan": "1.9.x-dev",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/browser-kit": "^6.0",
|
||||
"symfony/css-selector": "^6.0",
|
||||
@@ -103,6 +103,9 @@
|
||||
"files": [
|
||||
"src/Core/I18n/I18n.php"
|
||||
],
|
||||
"classmap": [
|
||||
"src/Core/Event/EventResult.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"App\\": "src/",
|
||||
"Plugin\\": "plugins/",
|
||||
@@ -112,7 +115,8 @@
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"App\\Tests\\": "tests/"
|
||||
"App\\Test\\Fixtures\\": "tests/fixtures/",
|
||||
"App\\Test\\": "tests/"
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
|
2673
composer.lock
generated
2673
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -20,15 +20,13 @@ security:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
oauth:
|
||||
pattern: ^/oauth
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: local_user
|
||||
form_login:
|
||||
login_path: security_login
|
||||
check_path: security_login
|
||||
default_target_path: root
|
||||
logout:
|
||||
path: security_logout
|
||||
# where to redirect after logout
|
||||
|
@@ -13,7 +13,10 @@ services:
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/*'
|
||||
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Routes}'
|
||||
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Routes,Core/Event/EventResult.php}'
|
||||
|
||||
App\Test\Fixtures\:
|
||||
resource: '../tests/fixtures/*'
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
@@ -21,7 +24,7 @@ services:
|
||||
resource: '../src/Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
App\Core\Router\RouteLoader:
|
||||
App\Core\Router:
|
||||
tags: ['routing.loader']
|
||||
|
||||
# Wrapper around Doctrine's StaticPHP metadata driver
|
||||
|
@@ -93,7 +93,7 @@ services:
|
||||
- ./docker/php/entrypoint.sh:/entrypoint.sh
|
||||
- ./docker/db/wait_for_db.sh:/wait_for_db.sh
|
||||
- ./docker/social/install.sh:/var/entrypoint.d/social_install.sh
|
||||
- ./docker/social/worker.sh:/var/entrypoint.d/social_worker.sh
|
||||
- ./docker/worker/worker.sh:/var/entrypoint.d/social_worker.sh
|
||||
# Main files
|
||||
- .:/var/www/social
|
||||
- /var/www/social/docker # exclude docker folder
|
||||
|
@@ -1,3 +1,18 @@
|
||||
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 {
|
||||
|
||||
listen [::]:80;
|
||||
@@ -5,6 +20,10 @@ server {
|
||||
|
||||
server_name %hostname%;
|
||||
|
||||
location '/.well-known/acme-challenge' {
|
||||
proxy_pass http://localhost:81;
|
||||
}
|
||||
|
||||
# redirect all traffic to HTTPS
|
||||
rewrite ^ https://$host$request_uri? permanent;
|
||||
}
|
||||
@@ -35,6 +54,13 @@ server {
|
||||
root /var/www/social;
|
||||
}
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
root /var/www/certbot;
|
||||
try_files $uri =404;
|
||||
break;
|
||||
}
|
||||
|
||||
# PHP
|
||||
location ~ ^/(index|install)\.php(/.*)?$ {
|
||||
include fastcgi_params;
|
||||
|
@@ -2,8 +2,7 @@
|
||||
|
||||
case "${DBMS}" in
|
||||
'postgres')
|
||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -ltq -Upostgres -hdb | \
|
||||
cut -d '|' -f1 | grep -Fwq "${SOCIAL_DB}"
|
||||
test "$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -Upostgres -hdb -tAc "select 1 from pg_database where datname='${SOCIAL_DB}'")" = "1"
|
||||
DB_EXISTS=$?
|
||||
;;
|
||||
'mariadb')
|
||||
@@ -28,7 +27,8 @@ if [ ${DB_EXISTS} -ne 0 ]; then
|
||||
chmod g+w -R .
|
||||
chown -R :www-data .
|
||||
|
||||
php bin/console doctrine:database:create || exit 1
|
||||
php bin/console doctrine:database:drop -f
|
||||
php bin/console doctrine:database:create
|
||||
php bin/console doctrine:schema:create || exit 1
|
||||
php bin/console app:populate_initial_values || exit 1
|
||||
|
||||
|
@@ -3,8 +3,10 @@
|
||||
cd /var/www/social || exit 1
|
||||
|
||||
printf "Cleaning Redis cache: " && echo "FLUSHALL" | nc redis 6379
|
||||
yes yes | php bin/console doctrine:fixtures:load || exit 1
|
||||
php bin/console app:populate_initial_values # since loading fixtures purges the DB
|
||||
bin/console doctrine:database:drop --force || exit 1
|
||||
bin/console doctrine:database:create || exit 1
|
||||
bin/console doctrine:schema:update --force || exit 1
|
||||
yes yes | bin/console doctrine:fixtures:load || exit 1
|
||||
|
||||
if [ "$#" -eq 0 ] || [ -z "$*" ]; then
|
||||
vendor/bin/simple-phpunit -vvv --coverage-html .test_coverage_report
|
||||
|
@@ -45,10 +45,10 @@ Example 1: Adding elements to the core UI
|
||||
* @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
|
||||
*
|
||||
* @return bool true if not handled or if the handling should be accumulated with other listeners,
|
||||
* @return \EventResult 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
|
||||
*/
|
||||
public function onViewAttachmentImage(array $vars, array &$res): bool
|
||||
public function onViewAttachmentImage(array $vars, array &$res): \EventResult
|
||||
{
|
||||
$res[] = Formatting::twigRenderFile('imageEncoder/imageEncoderView.html.twig', ['attachment' => $vars['attachment'], 'thumbnail_parameters' => $vars['thumbnail_parameters']]);
|
||||
return Event::stop;
|
||||
@@ -74,11 +74,25 @@ Event::handle('ResizerAvailable', [&$event_map]);
|
||||
/**
|
||||
* @param array $event_map output
|
||||
*
|
||||
* @return bool event hook
|
||||
* @return \EventResult event hook
|
||||
*/
|
||||
public function onResizerAvailable(array &$event_map): bool
|
||||
public function onResizerAvailable(array &$event_map): \EventResult
|
||||
{
|
||||
$event_map['image'] = 'ResizeImagePath';
|
||||
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
|
||||
}
|
||||
```
|
||||
|
50
phpstan.neon
50
phpstan.neon
@@ -1,5 +1,7 @@
|
||||
parameters:
|
||||
level: 4
|
||||
level: 6
|
||||
tmpDir: /var/www/social/var/cache/phpstan
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
bootstrapFiles:
|
||||
- config/bootstrap.php
|
||||
paths:
|
||||
@@ -15,6 +17,13 @@ parameters:
|
||||
earlyTerminatingMethodCalls:
|
||||
App\Core\Log:
|
||||
- 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:
|
||||
-
|
||||
message: '/Access to an undefined property App\\Util\\Bitmap::\$\w+/'
|
||||
@@ -35,10 +44,41 @@ parameters:
|
||||
paths:
|
||||
- *
|
||||
|
||||
# -
|
||||
# message: '/has no return typehint specified/'
|
||||
# paths:
|
||||
# - tests/*
|
||||
-
|
||||
message: '/::onCollectionQueryCreateExpression\(\) has parameter \$(actor|note)_expr with no type specified\./'
|
||||
paths:
|
||||
- *
|
||||
|
||||
-
|
||||
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:
|
||||
-
|
||||
|
@@ -34,15 +34,14 @@ namespace Plugin\ActivityPub;
|
||||
|
||||
use ActivityPhp\Type;
|
||||
use ActivityPhp\Type\AbstractObject;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\HTTPClient;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Modules\Plugin;
|
||||
use App\Core\Queue\Queue;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Queue;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
@@ -51,6 +50,7 @@ use App\Util\Exception\BugFoundException;
|
||||
use Component\Collection\Util\Controller\OrderedCollection;
|
||||
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
|
||||
use Component\FreeNetwork\Util\Discovery;
|
||||
use EventResult;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Plugin\ActivityPub\Controller\Inbox;
|
||||
@@ -104,7 +104,7 @@ class ActivityPub extends Plugin
|
||||
'User-Agent' => 'GNUsocialBot ' . GNUSOCIAL_VERSION . ' - ' . GNUSOCIAL_PROJECT_URL,
|
||||
];
|
||||
|
||||
public function version(): string
|
||||
public static function version(): string
|
||||
{
|
||||
return '3.0.0';
|
||||
}
|
||||
@@ -123,14 +123,14 @@ class ActivityPub extends Plugin
|
||||
],
|
||||
];
|
||||
|
||||
public function onInitializePlugin(): bool
|
||||
public function onInitializePlugin(): EventResult
|
||||
{
|
||||
Event::handle('ActivityStreamsTwoContext', [&self::$activity_streams_two_context]);
|
||||
self::$activity_streams_two_context = array_unique(self::$activity_streams_two_context, \SORT_REGULAR);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onQueueActivitypubInbox(ActivitypubActor $ap_actor, Actor $actor, string|AbstractObject $type): bool
|
||||
public function onQueueActivitypubInbox(ActivitypubActor $ap_actor, Actor $actor, string|AbstractObject $type): EventResult
|
||||
{
|
||||
// TODO: Check if Actor has authority over payload
|
||||
|
||||
@@ -141,14 +141,12 @@ class ActivityPub extends Plugin
|
||||
$ap_actor->getActorId(),
|
||||
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();
|
||||
if (Event::handle('ActivityPubNewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]) === Event::next) {
|
||||
Event::handle('NewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]);
|
||||
if (($att_targets = $ap_act->getAttentionTargets()) !== []) {
|
||||
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, $act, $att_targets, _m('{actor_id} triggered a notification via ActivityPub.', ['{nickname}' => $actor->getId()])]);
|
||||
}
|
||||
}
|
||||
|
||||
return Event::stop;
|
||||
@@ -158,9 +156,9 @@ class ActivityPub extends Plugin
|
||||
* 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.
|
||||
*
|
||||
* @param RouteLoader $r the router that was initialized
|
||||
* @param Router $r the router that was initialized
|
||||
*/
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect(
|
||||
'activitypub_inbox',
|
||||
@@ -186,7 +184,7 @@ class ActivityPub extends Plugin
|
||||
/**
|
||||
* Fill Actor->getUrl() calls with correct URL coming from ActivityPub
|
||||
*/
|
||||
public function onStartGetActorUri(Actor $actor, int $type, ?string &$url): bool
|
||||
public function onStartGetActorUri(Actor $actor, int $type, ?string &$url): EventResult
|
||||
{
|
||||
if (
|
||||
// Is remote?
|
||||
@@ -206,7 +204,7 @@ class ActivityPub extends Plugin
|
||||
/**
|
||||
* Fill Actor->canAdmin() for Actors that came from ActivityPub
|
||||
*/
|
||||
public function onFreeNetworkActorCanAdmin(Actor $actor, Actor $other, bool &$canAdmin): bool
|
||||
public function onFreeNetworkActorCanAdmin(Actor $actor, Actor $other, bool &$canAdmin): EventResult
|
||||
{
|
||||
// Are both in AP?
|
||||
if (
|
||||
@@ -226,7 +224,7 @@ class ActivityPub extends Plugin
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): bool
|
||||
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): EventResult
|
||||
{
|
||||
if (\count(array_intersect(self::$accept_headers, $accept_header)) === 0) {
|
||||
return Event::next;
|
||||
@@ -265,7 +263,7 @@ class ActivityPub extends Plugin
|
||||
/**
|
||||
* Add ActivityStreams 2 Extensions
|
||||
*/
|
||||
public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): bool
|
||||
public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): EventResult
|
||||
{
|
||||
switch ($type_name) {
|
||||
case 'Person':
|
||||
@@ -283,9 +281,11 @@ class ActivityPub extends Plugin
|
||||
/**
|
||||
* Let FreeNetwork Component know we exist and which class to use to call the freeNetworkDistribute method
|
||||
*/
|
||||
public function onAddFreeNetworkProtocol(array &$protocols): bool
|
||||
public function onAddFreeNetworkProtocol(array &$protocols): EventResult
|
||||
{
|
||||
if (!\in_array('\Plugin\ActivityPub\ActivityPub', $protocols)) {
|
||||
$protocols[] = '\Plugin\ActivityPub\ActivityPub';
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
@@ -296,11 +296,11 @@ class ActivityPub extends Plugin
|
||||
*
|
||||
* @return bool true if imported, false otherwise
|
||||
*/
|
||||
public static function freeNetworkGrabRemote(string $uri): bool
|
||||
public static function freeNetworkGrabRemote(string $uri, ?Actor $on_behalf_of = null): bool
|
||||
{
|
||||
if (Common::isValidHttpUrl($uri)) {
|
||||
try {
|
||||
$object = self::getObjectByUri($uri);
|
||||
$object = self::getObjectByUri($uri, try_online: true, on_behalf_of: $on_behalf_of);
|
||||
if (!\is_null($object)) {
|
||||
if ($object instanceof Type\AbstractObject) {
|
||||
if (\in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) {
|
||||
@@ -324,15 +324,24 @@ class ActivityPub extends Plugin
|
||||
string $inbox,
|
||||
array $to_actors,
|
||||
array &$retry_args,
|
||||
): bool
|
||||
{
|
||||
): EventResult {
|
||||
try {
|
||||
$data = Model::toJson($activity);
|
||||
if ($sender->isGroup()) {
|
||||
// When the sender is a group, we have to wrap it in an Announce activity
|
||||
$data = Type::create('Announce', ['object' => $data])->toJson();
|
||||
$data = Model::toType($activity);
|
||||
if ($sender->isGroup()) { // When the sender is a group,
|
||||
if ($activity->getVerb() === 'subscribe') {
|
||||
// Regular postman happens
|
||||
} 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, $inbox);
|
||||
}
|
||||
$res = self::postman($sender, $data->toJson(), $inbox);
|
||||
|
||||
// accumulate errors for later use, if needed
|
||||
$status_code = $res->getStatusCode();
|
||||
@@ -380,6 +389,7 @@ class ActivityPub extends Plugin
|
||||
// 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
|
||||
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;
|
||||
}
|
||||
$to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor;
|
||||
@@ -391,7 +401,7 @@ class ActivityPub extends Plugin
|
||||
foreach ($to_addr as $inbox => $to_actors) {
|
||||
Queue::enqueue(
|
||||
payload: [$sender, $activity, $inbox, $to_actors],
|
||||
queue: 'activitypub_postman',
|
||||
queue: 'ActivitypubPostman',
|
||||
priority: false,
|
||||
);
|
||||
}
|
||||
@@ -419,7 +429,7 @@ class ActivityPub extends Plugin
|
||||
/**
|
||||
* Add activity+json mimetype to WebFinger
|
||||
*/
|
||||
public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): bool
|
||||
public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): EventResult
|
||||
{
|
||||
if ($object->isPerson()) {
|
||||
$link = new XML_XRD_Element_Link(
|
||||
@@ -435,7 +445,7 @@ class ActivityPub extends Plugin
|
||||
/**
|
||||
* When FreeNetwork component asks us to help with identifying Actors from XRDs
|
||||
*/
|
||||
public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): bool
|
||||
public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): EventResult
|
||||
{
|
||||
$addr = null;
|
||||
foreach ($xrd->aliases as $alias) {
|
||||
@@ -466,7 +476,7 @@ class ActivityPub extends Plugin
|
||||
/**
|
||||
* When FreeNetwork component asks us to help with identifying Actors from URIs
|
||||
*/
|
||||
public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): bool
|
||||
public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): EventResult
|
||||
{
|
||||
try {
|
||||
if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) {
|
||||
@@ -530,7 +540,7 @@ class ActivityPub extends Plugin
|
||||
*
|
||||
* @return null|Actor|mixed|Note got from URI
|
||||
*/
|
||||
public static function getObjectByUri(string $resource, bool $try_online = true)
|
||||
public static function getObjectByUri(string $resource, bool $try_online = true, ?Actor $on_behalf_of = null): mixed
|
||||
{
|
||||
// Try known object
|
||||
$known_object = DB::findOneBy(ActivitypubObject::class, ['object_uri' => $resource], return_null: true);
|
||||
@@ -544,35 +554,49 @@ class ActivityPub extends Plugin
|
||||
return $known_activity->getActivity();
|
||||
}
|
||||
|
||||
// Try local Note
|
||||
if (Common::isValidHttpUrl($resource)) {
|
||||
$resource_parts = parse_url($resource);
|
||||
// TODO: Use URLMatcher
|
||||
if ($resource_parts['host'] === Common::config('site', 'server')) {
|
||||
$local_note = DB::findOneBy('note', ['url' => $resource], return_null: true);
|
||||
if (!\is_null($local_note)) {
|
||||
return $local_note;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try Actor
|
||||
try {
|
||||
return Explorer::getOneFromUri($resource, try_online: false);
|
||||
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
|
||||
}
|
||||
|
||||
// Try remote
|
||||
if (!$try_online) {
|
||||
return;
|
||||
// Is it a HTTP URL?
|
||||
if (Common::isValidHttpUrl($resource)) {
|
||||
$resource_parts = parse_url($resource);
|
||||
// If it is local
|
||||
if ($resource_parts['host'] === Common::config('site', 'server')) {
|
||||
// Try Local Note
|
||||
$local_note = DB::findOneBy(Note::class, ['url' => $resource], return_null: true);
|
||||
if (!\is_null($local_note)) {
|
||||
return $local_note;
|
||||
}
|
||||
|
||||
$response = HTTPClient::get($resource, ['headers' => self::HTTP_CLIENT_HEADERS]);
|
||||
// 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;
|
||||
return null;
|
||||
} elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
|
||||
throw new Exception('Non Ok Status Code for given Object id.');
|
||||
} else {
|
||||
@@ -580,3 +604,7 @@ class ActivityPub extends Plugin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -32,12 +32,13 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActivityPub\Controller;
|
||||
|
||||
use ActivityPhp\Type\AbstractObject;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Queue\Queue;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Queue;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
@@ -96,10 +97,12 @@ class Inbox extends Controller
|
||||
return $error('Actor not found in the request.');
|
||||
}
|
||||
|
||||
$to_actor = $this->deriveActivityTo($type);
|
||||
|
||||
try {
|
||||
$resource_parts = parse_url($type->get('actor'));
|
||||
if ($resource_parts['host'] !== Common::config('site', 'server')) {
|
||||
$actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor')));
|
||||
$actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor'), try_online: true, on_behalf_of: $to_actor));
|
||||
$ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]);
|
||||
} else {
|
||||
throw new Exception('Only remote actors can use this endpoint.');
|
||||
@@ -137,7 +140,7 @@ class Inbox extends Controller
|
||||
// If the signature fails verification the first time, update profile as it might have changed public key
|
||||
if ($verified !== 1) {
|
||||
try {
|
||||
$res = Explorer::getRemoteActorActivity($ap_actor->getUri());
|
||||
$res = Explorer::getRemoteActorActivity($ap_actor->getUri(), $to_actor);
|
||||
if (\is_null($res)) {
|
||||
return $error('Invalid remote actor (null response).');
|
||||
}
|
||||
@@ -164,10 +167,74 @@ class Inbox extends Controller
|
||||
|
||||
Queue::enqueue(
|
||||
payload: [$ap_actor, $actor, $type],
|
||||
queue: 'activitypub_inbox',
|
||||
queue: 'ActivitypubInbox',
|
||||
priority: false,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ namespace Plugin\ActivityPub\Controller;
|
||||
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Exception\ClientException;
|
||||
|
@@ -32,7 +32,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActivityPub\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Entity\Activity;
|
||||
use DateTimeInterface;
|
||||
@@ -101,27 +101,17 @@ class ActivitypubActivity extends Entity
|
||||
|
||||
public function getActivity(): Activity
|
||||
{
|
||||
return DB::findOneBy('activity', ['id' => $this->getActivityId()]);
|
||||
return DB::findOneBy(Activity::class, ['id' => $this->getActivityId()]);
|
||||
}
|
||||
|
||||
public array $_object_mention_ids = [];
|
||||
public function setObjectMentionIds(array $mentions): self
|
||||
public function getAttentionTargetIds(): array
|
||||
{
|
||||
$this->_object_mention_ids = $mentions;
|
||||
return $this;
|
||||
return $this->getActivity()->getAttentionTargetIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Entity->getNotificationTargetIds
|
||||
*/
|
||||
public function getNotificationTargetIds(array $ids_already_known = [], ?int $sender_id = null, bool $include_additional = true): array
|
||||
public function getAttentionTargets(): array
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
return $this->getActivity()->getAttentionTargets();
|
||||
}
|
||||
|
||||
public static function schemaDef(): array
|
||||
|
@@ -33,7 +33,7 @@ declare(strict_types = 1);
|
||||
namespace Plugin\ActivityPub\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
|
@@ -32,7 +32,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActivityPub\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
@@ -115,6 +115,16 @@ class ActivitypubObject extends Entity
|
||||
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
|
||||
{
|
||||
return [
|
||||
|
@@ -32,7 +32,7 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActivityPub\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Log;
|
||||
use App\Entity\Actor;
|
||||
|
70
plugins/ActivityPub/Test/Fixtures/ActivityPubFixtures.php
Normal file
70
plugins/ActivityPub/Test/Fixtures/ActivityPubFixtures.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
82
plugins/ActivityPub/Test/Fixtures/friendica/activities/create_note.jsonld
Executable file
82
plugins/ActivityPub/Test/Fixtures/friendica/activities/create_note.jsonld
Executable file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||
"dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
|
||||
"diaspora": "https://diasporafoundation.org/ns/",
|
||||
"litepub": "http://litepub.social/ns#",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"schema": "http://schema.org#",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"sensitive": "as:sensitive",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"directMessage": "litepub:directMessage",
|
||||
"discoverable": "toot:discoverable",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value"
|
||||
}
|
||||
],
|
||||
"id": "https://soc.schuerz.at/objects/4edd2508-4361-edb8-c4d8-b45181083984/Create",
|
||||
"type": "Create",
|
||||
"actor": "https://soc.schuerz.at/profile/jakob",
|
||||
"published": "2022-01-23T20:21:24Z",
|
||||
"instrument": {
|
||||
"type": "Service",
|
||||
"name": "Friendica 'Siberian Iris' 2021.12-rc-1448",
|
||||
"url": "https://soc.schuerz.at"
|
||||
},
|
||||
"to": [
|
||||
"https://lemmy.schuerz.at/u/jakob",
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://lemmy.schuerz.at/c/test"
|
||||
],
|
||||
"cc": [
|
||||
"https://soc.schuerz.at/followers/jakob"
|
||||
],
|
||||
"object": {
|
||||
"id": "https://soc.schuerz.at/objects/4edd2508-4361-edb8-c4d8-b45181083984",
|
||||
"type": "Note",
|
||||
"summary": "",
|
||||
"inReplyTo": "https://lemmy.schuerz.at/post/25360",
|
||||
"diaspora:guid": "4edd2508-4361-edb8-c4d8-b45181083984",
|
||||
"published": "2022-01-23T20:21:24Z",
|
||||
"url": "https://soc.schuerz.at/display/4edd2508-4361-edb8-c4d8-b45181083984",
|
||||
"attributedTo": "https://soc.schuerz.at/profile/jakob",
|
||||
"sensitive": false,
|
||||
"context": "https://lemmy.schuerz.at/post/25360#context",
|
||||
"content": "<span class=\"h-card\"><a href=\"https://lemmy.schuerz.at/u/jakob\" class=\"u-url mention\">@<span>jakob</span></a></span> test",
|
||||
"contentMap": {
|
||||
"de": "<bdi>@<a href=\"https://lemmy.schuerz.at/u/jakob\" class=\"userinfo mention\" title=\"jakob\">jakob</a></bdi> test"
|
||||
},
|
||||
"source": {
|
||||
"content": "@[url=https://lemmy.schuerz.at/u/jakob]Jakob[/url] test",
|
||||
"mediaType": "text/bbcode"
|
||||
},
|
||||
"diaspora:comment": "{\"author\":\"jakob@soc.schuerz.at\",\"guid\":\"4edd2508-4361-edb8-c4d8-b45181083984\",\"created_at\":\"2022-01-23T20:21:24Z\",\"edited_at\":\"2022-01-23T20:21:24Z\",\"parent_guid\":\"ea620d1e-742c8b4d15249a9b-18b5fca3\",\"text\":\"@{Jakob; jakob@lemmy.schuerz.at} test\",\"author_signature\":\"JNCqOui5Cg8\\/Uxw+f0NtGCRjRnhPOrqE6kGJnMkZvOOKhlCdZbCqvyPlNJzEYDa3Z30mOWQKTTNo5BVI+VVZtGrVEqFOdzNog7jOLQoY1dKU9iEQ9vc8USwUCkyJyv48w1iXpfea87KPwv+03DMlftmD6kC7jdUVwhc7+jm0g4fh06tpOcCMQJOZqTTV\\/80EjxIJQ+8eEk5evSw\\/S98ohD1ahcwSomJ9hJUV1H48ucDvMod1FCLcN5h4ALHqubCu4TZIYhGhw9zoCl52GeHhrD3\\/vL6OW4ftZ7UG4rEKQ4HowuXqmNwydrQldtprRtu2UrZBjLqVusPXEs\\/xERQqZnalNXHijyd1TwwCmfTV4YjKwH4BhX\\/p4hdWMqEP4yYXlfA4apalVeAaYZLrNR58kPJjBHad\\/yqH30ziBFheqZ5odFh\\/jnKB4OCFVST3u9b1OKE0jyTrbTepPTaONwc8giQH1sM8koj1gFdulwuJuOTRUKR\\/8ishgHi5SWwbp5YG5Z3YSINkF10IcLiFZAF300AvwgOCdf7ferim4i\\/7TR1D2CBpoNUZnKCKZRymZbE0GuKEE+A6Pk3lk\\/DCsDtmMXpnxlPZ8Nq8OZS\\/olXevAu1y57MNnxBDXtojr4F54MP2fO7E2JwBr7AlwoeSEvtZSAO\\/elzrKfW0eVWOUM2OnI=\"}",
|
||||
"attachment": [],
|
||||
"tag": [
|
||||
{
|
||||
"type": "Mention",
|
||||
"href": "https://lemmy.schuerz.at/u/jakob",
|
||||
"name": "@jakob@lemmy.schuerz.at"
|
||||
}
|
||||
],
|
||||
"to": [
|
||||
"https://lemmy.schuerz.at/u/jakob",
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://lemmy.schuerz.at/c/test"
|
||||
],
|
||||
"cc": [
|
||||
"https://soc.schuerz.at/followers/jakob"
|
||||
]
|
||||
},
|
||||
"signature": {
|
||||
"type": "RsaSignature2017",
|
||||
"nonce": "fe42f1478453c9c5e92efdc8a1b00c7e2dd2ce89501f2437c4438b8add1c8ff7",
|
||||
"creator": "https://soc.schuerz.at/profile/jakob#main-key",
|
||||
"created": "2022-01-23T20:21:25Z",
|
||||
"signatureValue": "iWeNKyfH/d5+f6FDmZIadF4hW7XBliL8w3PQ2QkeKQG7fheqx1MB6825JX+Eaq8C0aNESesTTiDJgy3Xdcw8tgKwAVdji2DNZh7rNbSy57AzXlXOPRDnGJUbXp8gAuW2PJNZx3TTsJ5yM7tKLmHk0PpwsnKbvjFabL5O+htyfRZNVjFAsB9bVym/dBvf4jiTZiLufGDprgsaDVygUi3QrzmwsE41NZtL/MIEtbiC5pROWQvdQBEzeLfMDsnjI4CR+3tnaSlvepipuFxeSFpwl5Ae5+YM6IYRvSDsssjr8kAg1t3XnHUyeBdBdys0A6ryR5t5QuY0ygAHFs+X633JsgHDuCxxHiqNYxFuTs1xO0gmHydFy1iKlEt2rbr9pcX05hSvEFg0bI8HEC5M9GuafpY7sOyLX0jobBUH9CxdHUu0qri4ntORlvvAYsGFNHj+folFlMRBNMkcZ+MbrAxdoXBdjhsAp+tD6nje+PeZy63yJJQmPLQi9E+fHGGe0DAobGrBE/XF8X1ABH+ywyKwVu0t6lkSxu+zdr9+JXKgnf7HaFSsknapumw9aQwC7N/Q0M5KO41fF0R4VL2GtoppyB9Ck9Dg1zwMWjL2KZN3ckbWABb+frWtmKIVQACzupRWzHiHSZjRRNJalK3uugVisHF2PFGkjYoUjHDCNegKHO0="
|
||||
}
|
||||
}
|
56
plugins/ActivityPub/Test/Fixtures/friendica/objects/note.jsonld
Executable file
56
plugins/ActivityPub/Test/Fixtures/friendica/objects/note.jsonld
Executable file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||
"dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
|
||||
"diaspora": "https://diasporafoundation.org/ns/",
|
||||
"litepub": "http://litepub.social/ns#",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"schema": "http://schema.org#",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"sensitive": "as:sensitive",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"directMessage": "litepub:directMessage",
|
||||
"discoverable": "toot:discoverable",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value"
|
||||
}
|
||||
],
|
||||
"id": "https://soc.schuerz.at/objects/4edd2508-4361-edb8-c4d8-b45181083984",
|
||||
"type": "Note",
|
||||
"summary": "",
|
||||
"inReplyTo": "https://lemmy.schuerz.at/post/25360",
|
||||
"diaspora:guid": "4edd2508-4361-edb8-c4d8-b45181083984",
|
||||
"published": "2022-01-23T20:21:24Z",
|
||||
"url": "https://soc.schuerz.at/display/4edd2508-4361-edb8-c4d8-b45181083984",
|
||||
"attributedTo": "https://soc.schuerz.at/profile/jakob",
|
||||
"sensitive": false,
|
||||
"context": "https://lemmy.schuerz.at/post/25360#context",
|
||||
"content": "<span class=\"h-card\"><a href=\"https://lemmy.schuerz.at/u/jakob\" class=\"u-url mention\">@<span>jakob</span></a></span> test",
|
||||
"contentMap": {
|
||||
"de": "<bdi>@<a href=\"https://lemmy.schuerz.at/u/jakob\" class=\"userinfo mention\" title=\"jakob\">jakob</a></bdi> test"
|
||||
},
|
||||
"source": {
|
||||
"content": "@[url=https://lemmy.schuerz.at/u/jakob]Jakob[/url] test",
|
||||
"mediaType": "text/bbcode"
|
||||
},
|
||||
"diaspora:comment": "{\"author\":\"jakob@soc.schuerz.at\",\"guid\":\"4edd2508-4361-edb8-c4d8-b45181083984\",\"created_at\":\"2022-01-23T20:21:24Z\",\"edited_at\":\"2022-01-23T20:21:24Z\",\"parent_guid\":\"ea620d1e-742c8b4d15249a9b-18b5fca3\",\"text\":\"@{Jakob; jakob@lemmy.schuerz.at} test\",\"author_signature\":\"JNCqOui5Cg8\\/Uxw+f0NtGCRjRnhPOrqE6kGJnMkZvOOKhlCdZbCqvyPlNJzEYDa3Z30mOWQKTTNo5BVI+VVZtGrVEqFOdzNog7jOLQoY1dKU9iEQ9vc8USwUCkyJyv48w1iXpfea87KPwv+03DMlftmD6kC7jdUVwhc7+jm0g4fh06tpOcCMQJOZqTTV\\/80EjxIJQ+8eEk5evSw\\/S98ohD1ahcwSomJ9hJUV1H48ucDvMod1FCLcN5h4ALHqubCu4TZIYhGhw9zoCl52GeHhrD3\\/vL6OW4ftZ7UG4rEKQ4HowuXqmNwydrQldtprRtu2UrZBjLqVusPXEs\\/xERQqZnalNXHijyd1TwwCmfTV4YjKwH4BhX\\/p4hdWMqEP4yYXlfA4apalVeAaYZLrNR58kPJjBHad\\/yqH30ziBFheqZ5odFh\\/jnKB4OCFVST3u9b1OKE0jyTrbTepPTaONwc8giQH1sM8koj1gFdulwuJuOTRUKR\\/8ishgHi5SWwbp5YG5Z3YSINkF10IcLiFZAF300AvwgOCdf7ferim4i\\/7TR1D2CBpoNUZnKCKZRymZbE0GuKEE+A6Pk3lk\\/DCsDtmMXpnxlPZ8Nq8OZS\\/olXevAu1y57MNnxBDXtojr4F54MP2fO7E2JwBr7AlwoeSEvtZSAO\\/elzrKfW0eVWOUM2OnI=\"}",
|
||||
"attachment": [],
|
||||
"tag": [
|
||||
{
|
||||
"type": "Mention",
|
||||
"href": "https://lemmy.schuerz.at/u/jakob",
|
||||
"name": "@jakob@lemmy.schuerz.at"
|
||||
}
|
||||
],
|
||||
"to": [
|
||||
"https://lemmy.schuerz.at/u/jakob",
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://lemmy.schuerz.at/c/test"
|
||||
],
|
||||
"cc": [
|
||||
"https://soc.schuerz.at/followers/jakob"
|
||||
]
|
||||
}
|
94
plugins/ActivityPub/Test/Fixtures/friendica/objects/person.jsonld
Executable file
94
plugins/ActivityPub/Test/Fixtures/friendica/objects/person.jsonld
Executable file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#",
|
||||
"dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
|
||||
"diaspora": "https://diasporafoundation.org/ns/",
|
||||
"litepub": "http://litepub.social/ns#",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"schema": "http://schema.org#",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"sensitive": "as:sensitive",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"directMessage": "litepub:directMessage",
|
||||
"discoverable": "toot:discoverable",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value"
|
||||
}
|
||||
],
|
||||
"id": "https://soc.schuerz.at/profile/jakob",
|
||||
"diaspora:guid": "4edd2508-1661-30f6-ebcc-2da966353356",
|
||||
"type": "Person",
|
||||
"following": "https://soc.schuerz.at/following/jakob",
|
||||
"followers": "https://soc.schuerz.at/followers/jakob",
|
||||
"inbox": "https://soc.schuerz.at/inbox/jakob",
|
||||
"outbox": "https://soc.schuerz.at/outbox/jakob",
|
||||
"preferredUsername": "jakob",
|
||||
"name": "Jakob :friendica:",
|
||||
"vcard:hasAddress": {
|
||||
"@type": "vcard:Home",
|
||||
"vcard:country-name": "Austria",
|
||||
"vcard:region": "Niederoesterreich",
|
||||
"vcard:locality": ""
|
||||
},
|
||||
"summary": "Linux, FOSS, Öffentlicher Verkehr, Eisenbahn, Radfahren, Fußgehen, Verkehrsplanung, Städtebau, Will das Schöne wieder in die Welt bringen,Nachhaltigkeit, Modellbahn, Java Entwickler (jun), Bash,<br><br>#FediverseOnlyAccount",
|
||||
"vcard:hasInstantMessage": [
|
||||
"xmpp:jakob@schuerz.at",
|
||||
"matrix:@jakob:schuerz.at"
|
||||
],
|
||||
"url": "https://soc.schuerz.at/profile/jakob",
|
||||
"manuallyApprovesFollowers": true,
|
||||
"discoverable": true,
|
||||
"publicKey": {
|
||||
"id": "https://soc.schuerz.at/profile/jakob#main-key",
|
||||
"owner": "https://soc.schuerz.at/profile/jakob",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1RRoj3DpUmTiRBshv+kz\njO5tgfHs99aBJjvaoW8nbPcOs+HZm9Nj4ncJh99kwd+yONwac6ObMMIisYpVU4C1\neKpnlRrRu/8vQFwhHQT4RxpkibB+l+LvG1HJoMNIuYxvVCIaQZugdJclAdMJjDTF\nbDQNwG6xlcazKd4IbMbmgfoxTxSnQSomJQew1NUbdD3vDiCdJEtjCmeWm6eTCfyZ\njT0mjrAm8ccJ7+opN5SWJ0je0Rav5dohyaVFEtv1Dlv1UlqU4hKefvv71eoROHCA\nWQ3+kYGFGY4ApnbWxwLZyke7khzxr2BjDrfwUAeEsLJT4YOxa5fKJJ59+q5Iddaq\nPNT3QqP0Qzum5w6qDOWm3cNNw7ByqoqxKckZS5U2vm0sx83UEmBqysAkAS/8M9Qr\nBKkb9DQ9jgUa7GPpL+Oknr8hV+Vpk49Jjx+A1WJ/MlNja7fi4w4rBM+v3B8nRayM\nzX8XaKbbOib21mCawJiJIOAm0EP2rNqNM1GpUWPstHKG00o3Czz3P5Hm/q6RcNJE\nKRlSIPQZnUVsoC0bFsqWzipsgb3uDHnz3Ni2OjLNLWBVYkWD7RNfB3WV/XKl2QL3\nnnhmUDahGN7UCOrcBuLfWsTa+GZDFeHot1HXa9tNcxq+QxAUg3qv7oiAH1H+hoJg\nn/Ydg1IR5sLovKi3g7DRS7MCAwEAAQ==\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://soc.schuerz.at/inbox"
|
||||
},
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"url": "https://soc.schuerz.at/photo/profile/jakob.png?ts=1630598950",
|
||||
"mediaType": "image/png"
|
||||
},
|
||||
"attachment": [
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Mobilizon",
|
||||
"value": "@jakob@events.schuerz.at<br>@jakob@events.tulln.social"
|
||||
},
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Lemmy",
|
||||
"value": "<a href=\"https://lemmy.schuerz.at/u/jakob\" target=\"_blank\" rel=\"noopener noreferrer\">https://lemmy.schuerz.at/u/jakob</a>"
|
||||
},
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Funkwhale",
|
||||
"value": "<a href=\"https://radio.schuerz.at/@jakob/\" target=\"_blank\" rel=\"noopener noreferrer\">https://radio.schuerz.at/@jakob/</a>"
|
||||
},
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Peertube",
|
||||
"value": "<a href=\"https://kino.schuerz.at/a/jakob\" target=\"_blank\" rel=\"noopener noreferrer\">https://kino.schuerz.at/a/jakob</a>"
|
||||
},
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Pixelfed",
|
||||
"value": "<a href=\"https://japix.schuerz.at/jakob\" target=\"_blank\" rel=\"noopener noreferrer\">https://japix.schuerz.at/jakob</a>"
|
||||
},
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "about:",
|
||||
"value": "This is an OpenPGP proof that connects my OpenPGP key to this Peertube account. For details check out <a href=\"https://keyoxide.org/guides/openpgp-proofs\" target=\"_blank\" rel=\"noopener noreferrer\">https://keyoxide.org/guides/openpgp-proofs</a><br><br>[Verifying my OpenPGP key: openpgp4fpr:FED82F1C73FF53FB1EE9926336615E0FD12833CF]"
|
||||
}
|
||||
],
|
||||
"generator": {
|
||||
"type": "Service",
|
||||
"name": "Friendica 'Siberian Iris' 2021.12-rc-1448",
|
||||
"url": "https://soc.schuerz.at"
|
||||
}
|
||||
}
|
53
plugins/ActivityPub/Test/Fixtures/gnusocial/activities/create_article.jsonld
Executable file
53
plugins/ActivityPub/Test/Fixtures/gnusocial/activities/create_article.jsonld
Executable file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user