Compare commits
20 Commits
experiment
...
v3
Author | SHA1 | Date | |
---|---|---|---|
|
49a80a3c40 | ||
|
97114e38e0
|
||
|
2df30e2987
|
||
|
3b3ded5212
|
||
|
dc240fae49
|
||
5cbb1627f2
|
|||
46ff8aacd2
|
|||
c4d6df4637
|
|||
053bc38792
|
|||
2fd46ca886
|
|||
c31f3d4997
|
|||
|
e6bb418fe6
|
||
fed2242a56
|
|||
edeee49af9
|
|||
4d7742e0e1
|
|||
76f2cdd212
|
|||
a2aa45fb1f
|
|||
d4b7e990ce
|
|||
aef1fac536
|
|||
556ac85061
|
@@ -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.
|
||||
|
@@ -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(Router $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:'])) {
|
||||
|
@@ -32,15 +32,17 @@ 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(Router $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']);
|
||||
@@ -49,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[] = [
|
||||
@@ -64,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) {
|
||||
@@ -127,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
|
||||
{
|
||||
|
@@ -39,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;
|
||||
|
||||
@@ -53,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(Router $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
|
||||
@@ -93,7 +95,7 @@ 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[] = [
|
||||
@@ -106,7 +108,10 @@ class Circle extends Component
|
||||
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) {
|
||||
@@ -118,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);
|
||||
@@ -129,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([
|
||||
@@ -145,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([
|
||||
@@ -155,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);
|
||||
@@ -168,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);
|
||||
@@ -187,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']);
|
||||
}
|
||||
@@ -201,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
|
||||
{
|
||||
@@ -218,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]));
|
||||
|
@@ -31,16 +31,20 @@ 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);
|
||||
|
@@ -25,6 +25,7 @@ use App\Core\Cache;
|
||||
use App\Core\DB;
|
||||
use App\Core\Entity;
|
||||
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(
|
||||
|
@@ -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,
|
||||
|
@@ -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));
|
||||
|
@@ -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
|
||||
{
|
||||
}
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -40,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(Router $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']);
|
||||
@@ -95,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;
|
||||
@@ -139,12 +140,13 @@ class Conversation extends Component
|
||||
/**
|
||||
* 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,10 +196,8 @@ 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 = $this->getReplyToIdFromRequest($request);
|
||||
if (!\is_null($to_note_id)) {
|
||||
@@ -211,14 +211,12 @@ class Conversation extends Component
|
||||
/**
|
||||
* Posting event to add extra information to Component\Posting form data
|
||||
*
|
||||
* @param array $data Transport data to be filled with reply_to_id
|
||||
* @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
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onPostingModifyData(Request $request, Actor $actor, array &$data): bool
|
||||
public function onPostingModifyData(Request $request, Actor $actor, array &$data): EventResult
|
||||
{
|
||||
$to_note_id = $this->getReplyToIdFromRequest($request);
|
||||
if (!\is_null($to_note_id)) {
|
||||
@@ -231,8 +229,10 @@ class Conversation extends Component
|
||||
|
||||
/**
|
||||
* Add minimal Note card to RightPanel template
|
||||
*
|
||||
* @param string[] $elements
|
||||
*/
|
||||
public function onPrependPostingForm(Request $request, array &$elements): bool
|
||||
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;
|
||||
@@ -244,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']);
|
||||
@@ -260,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;
|
||||
@@ -299,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;
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -27,10 +27,11 @@ use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router;
|
||||
use Component\Feed\Controller as C;
|
||||
use EventResult;
|
||||
|
||||
class Feed extends Component
|
||||
{
|
||||
public function onAddRoute(Router $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']);
|
||||
|
@@ -54,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;
|
||||
@@ -79,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(Router $m): bool
|
||||
public function onAddRoute(Router $m): EventResult
|
||||
{
|
||||
// Feeds
|
||||
$m->connect('feed_network', '/feed/network', [Feeds::class, 'network']);
|
||||
@@ -111,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++]));
|
||||
@@ -119,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 {
|
||||
@@ -147,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.
|
||||
@@ -223,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(
|
||||
@@ -243,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();
|
||||
@@ -257,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');
|
||||
@@ -275,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;
|
||||
@@ -343,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
|
||||
@@ -374,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 = [];
|
||||
|
||||
@@ -495,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) {
|
||||
@@ -516,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) {
|
||||
@@ -530,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',
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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
|
||||
{
|
||||
@@ -96,6 +101,8 @@ class GroupFeed extends FeedController
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function groupViewId(Request $request, int $id): array
|
||||
{
|
||||
@@ -119,6 +126,8 @@ class GroupFeed extends FeedController
|
||||
*
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function groupViewNickname(Request $request, string $nickname): array
|
||||
{
|
||||
|
@@ -33,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(Router $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']);
|
||||
@@ -49,8 +50,10 @@ 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 onNewNotificationStart(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()) {
|
||||
@@ -69,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'];
|
||||
@@ -108,7 +114,10 @@ class Group extends Component
|
||||
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)) {
|
||||
@@ -127,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)) {
|
||||
|
@@ -100,6 +100,8 @@ class Language extends Controller
|
||||
* @throws NoLoggedInUser
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function sortLanguages(Request $request): array
|
||||
{
|
||||
|
@@ -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(
|
||||
|
@@ -134,6 +134,9 @@ class Language extends Entity
|
||||
return self::getById($note->getLanguageId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getLanguageChoices(): array
|
||||
{
|
||||
$langs = Cache::getHashMap(
|
||||
@@ -144,6 +147,8 @@ class Language extends Entity
|
||||
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
|
||||
{
|
||||
|
@@ -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(Router $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');
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -32,21 +32,24 @@ 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(Router $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);
|
||||
|
@@ -84,6 +84,8 @@ class NoteToLink extends Entity
|
||||
* Create an instance of NoteToLink or fill in the
|
||||
* properties of $obj with the associative array $args. Doesn't
|
||||
* persist the result
|
||||
*
|
||||
* @param (array{link_id: int, note_id: int} & array<string, mixed>) $args
|
||||
*/
|
||||
public static function create(array $args, bool $_delegated_call = false): static
|
||||
{
|
||||
|
@@ -31,6 +31,7 @@ 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
|
||||
@@ -38,7 +39,7 @@ class Link extends Component
|
||||
/**
|
||||
* Note that this persists both a Link and a NoteToLink
|
||||
*
|
||||
* @return [Entity\Link, NoteToLink]
|
||||
* @return array{ link: ?Entity\Link, note_to_link: ?NoteToLink }
|
||||
*/
|
||||
public static function maybeCreateLink(string $url, int $note_id): array
|
||||
{
|
||||
@@ -53,8 +54,10 @@ class Link extends Component
|
||||
|
||||
/**
|
||||
* 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 = []): bool
|
||||
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')) {
|
||||
@@ -65,13 +68,13 @@ class Link extends Component
|
||||
if (\in_array($match, $ignore)) {
|
||||
continue;
|
||||
}
|
||||
self::maybeCreateLink($match, $note_id);
|
||||
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;
|
||||
@@ -149,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 = [
|
||||
@@ -196,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
|
||||
@@ -275,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;
|
||||
|
@@ -44,6 +44,8 @@ class Feed extends Controller
|
||||
{
|
||||
/**
|
||||
* Everything with attention to current user
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function notifications(Request $request): array
|
||||
{
|
||||
|
@@ -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'
|
||||
|
@@ -34,12 +34,13 @@ 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(Router $m): bool
|
||||
public function onAddRoute(Router $m): EventResult
|
||||
{
|
||||
$m->connect('feed_notifications', '/feed/notifications', [Feed::class, 'notifications']);
|
||||
return Event::next;
|
||||
@@ -48,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,
|
||||
@@ -72,10 +73,10 @@ class Notification extends Component
|
||||
*
|
||||
* @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 array $targets Attentions, Mentions, any other source. Should never be empty, you usually want to register an attention to every $sender->getSubscribers()
|
||||
* @param non-empty-array<Actor|int> $targets Attentions, Mentions, any other source. Should never be empty, you usually want to register an attention to every $sender->getSubscribers()
|
||||
* @param null|string $reason An optional reason explaining why this notification exists
|
||||
*/
|
||||
public function onNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
|
||||
public function onNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): EventResult
|
||||
{
|
||||
// Ensure targets are all actor objects and unique
|
||||
$effective_targets = [];
|
||||
@@ -102,13 +103,20 @@ class Notification extends Component
|
||||
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;
|
||||
@@ -121,7 +129,9 @@ class Notification extends Component
|
||||
* 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
|
||||
{
|
||||
|
@@ -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 [
|
||||
|
@@ -88,6 +88,8 @@ class PersonSettings extends Controller
|
||||
* @throws NoLoggedInUser
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public function allSettings(Request $request, LanguageController $language): array
|
||||
{
|
||||
@@ -205,6 +207,8 @@ class PersonSettings extends Controller
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
* @throws NoLoggedInUser
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return ControllerResultType[]
|
||||
*/
|
||||
private static function notifications(Request $request): array
|
||||
{
|
||||
|
@@ -26,10 +26,11 @@ use App\Core\Modules\Component;
|
||||
use App\Core\Router;
|
||||
use App\Util\Nickname;
|
||||
use Component\Person\Controller as C;
|
||||
use EventResult;
|
||||
|
||||
class Person extends Component
|
||||
{
|
||||
public function onAddRoute(Router $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,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();
|
||||
@@ -64,10 +60,6 @@ class PersonSettingsTest extends GNUsocialTestCase
|
||||
// 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();
|
||||
|
@@ -33,6 +33,7 @@ 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;
|
||||
@@ -43,10 +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 EventResult;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -54,7 +58,7 @@ class Posting extends Component
|
||||
{
|
||||
public const route = 'posting_form_action';
|
||||
|
||||
public function onAddRoute(Router $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect(self::route, '/form/posting', Controller\Posting::class);
|
||||
return Event::next;
|
||||
@@ -64,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;
|
||||
@@ -82,9 +88,25 @@ 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 storeLocalArticle(
|
||||
Actor $actor,
|
||||
@@ -147,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 $attentions 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
|
||||
@@ -160,7 +182,7 @@ class Posting extends Component
|
||||
* @throws DuplicateFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array [Activity, Note, Effective Attentions]
|
||||
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
|
||||
*/
|
||||
public static function storeLocalNote(
|
||||
Actor $actor,
|
||||
@@ -179,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]);
|
||||
@@ -298,7 +322,10 @@ class Posting extends Component
|
||||
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;
|
||||
|
@@ -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',
|
||||
|
@@ -39,11 +39,12 @@ 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(Router $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']);
|
||||
@@ -57,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
|
||||
{
|
||||
@@ -177,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),
|
||||
|
@@ -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]));
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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,14 +61,17 @@ class Tag extends Component
|
||||
public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags
|
||||
public const TAG_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']);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public static function maybeCreateTag(string $tag, int $note_id, ?int $lang_id): ?NoteTag
|
||||
/**
|
||||
* @param array{tag_use_canonical?: bool} $extra_args
|
||||
*/
|
||||
public static function maybeCreateTag(string $tag, int $note_id, ?int $lang_id, array $extra_args = []): ?NoteTag
|
||||
{
|
||||
if (!self::validate($tag)) {
|
||||
return null; // Ignore invalid tag candidates
|
||||
@@ -117,8 +121,10 @@ class Tag extends Component
|
||||
|
||||
/**
|
||||
* 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): bool
|
||||
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): EventResult
|
||||
{
|
||||
if ($extra_args['TagProcessed'] ?? false) {
|
||||
return Event::next;
|
||||
@@ -135,7 +141,7 @@ class Tag extends Component
|
||||
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;
|
||||
@@ -212,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;
|
||||
@@ -252,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');
|
||||
@@ -263,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/",
|
||||
|
2642
composer.lock
generated
2642
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ 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/*'
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
-
|
||||
|
@@ -50,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;
|
||||
@@ -122,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
|
||||
|
||||
@@ -157,7 +158,7 @@ class ActivityPub extends Plugin
|
||||
*
|
||||
* @param Router $r the router that was initialized
|
||||
*/
|
||||
public function onAddRoute(Router $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect(
|
||||
'activitypub_inbox',
|
||||
@@ -183,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?
|
||||
@@ -203,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 (
|
||||
@@ -223,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;
|
||||
@@ -262,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':
|
||||
@@ -280,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;
|
||||
}
|
||||
|
||||
@@ -293,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))) {
|
||||
@@ -321,7 +324,7 @@ class ActivityPub extends Plugin
|
||||
string $inbox,
|
||||
array $to_actors,
|
||||
array &$retry_args,
|
||||
): bool {
|
||||
): EventResult {
|
||||
try {
|
||||
$data = Model::toType($activity);
|
||||
if ($sender->isGroup()) { // When the sender is a group,
|
||||
@@ -426,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(
|
||||
@@ -442,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) {
|
||||
@@ -473,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))) {
|
||||
@@ -537,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): mixed
|
||||
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);
|
||||
@@ -553,7 +556,7 @@ class ActivityPub extends Plugin
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -589,7 +592,7 @@ class ActivityPub extends Plugin
|
||||
throw new Exception("Remote resource {$resource} not found without online resources.");
|
||||
}
|
||||
|
||||
$response = HTTPClient::get($resource, ['headers' => self::HTTP_CLIENT_HEADERS]);
|
||||
$response = Explorer::get($resource, $on_behalf_of);
|
||||
// If it was deleted
|
||||
if ($response->getStatusCode() == 410) {
|
||||
//$obj = Type::create('Tombstone', ['id' => $resource]);
|
||||
|
@@ -32,12 +32,14 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActivityPub\Controller;
|
||||
|
||||
use ActivityPhp\Type\AbstractObject;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Queue;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use Exception;
|
||||
@@ -95,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.');
|
||||
@@ -136,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).');
|
||||
}
|
||||
@@ -169,4 +173,68 @@ class Inbox extends Controller
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -3,9 +3,9 @@
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams"
|
||||
],
|
||||
"id": "https://another_instance.gnusocial.test/activity/41362",
|
||||
"id": "https://another-instance.gnusocial.test/activity/41362",
|
||||
"published": "2022-03-20T17:54:15+00:00",
|
||||
"actor": "https://another_instance.gnusocial.test/actor/43",
|
||||
"actor": "https://another-instance.gnusocial.test/actor/43",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
|
@@ -20,23 +20,23 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": "https://another_instance.gnusocial.test/actor/43",
|
||||
"inbox": "https://another_instance.gnusocial.test/actor/43/inbox.json",
|
||||
"outbox": "https://another_instance.gnusocial.test/actor/43/outbox.json",
|
||||
"following": "https://another_instance.gnusocial.test/actor/43/subscriptions",
|
||||
"followers": "https://another_instance.gnusocial.test/actor/43/subscribers",
|
||||
"liked": "https://another_instance.gnusocial.test/actor/43/favourites",
|
||||
"id": "https://another-instance.gnusocial.test/actor/43",
|
||||
"inbox": "https://another-instance.gnusocial.test/actor/43/inbox.json",
|
||||
"outbox": "https://another-instance.gnusocial.test/actor/43/outbox.json",
|
||||
"following": "https://another-instance.gnusocial.test/actor/43/subscriptions",
|
||||
"followers": "https://another-instance.gnusocial.test/actor/43/subscribers",
|
||||
"liked": "https://another-instance.gnusocial.test/actor/43/favourites",
|
||||
"preferredUsername": "alice",
|
||||
"publicKey": {
|
||||
"id": "https://another_instance.gnusocial.test/actor/43#public-key",
|
||||
"owner": "https://another_instance.gnusocial.test/actor/43",
|
||||
"id": "https://another-instance.gnusocial.test/actor/43#public-key",
|
||||
"owner": "https://another-instance.gnusocial.test/actor/43",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArRHP8lxqj1HqFL4q7CKh\noDyBvuhoaoWo/AGdjiWu5AEODL6utaQX+bCJApH9+XNICCkWvayKupgOvLPqBxkh\nl4TPUjlb1iCt+iZeMB8ftude4epaUNUDdqK1zG3g8z8AdF3nx9/cHI+8UY7+JAh6\naZ5EBi+wNYtl4UoDfizmLeRmmGIam5UQ6x2RseYCevIm1BBCZZHLdIaoPJphyjLW\n8sRJtHL4D3m28NkGG8GizctXHbMl7+RlVJ8HyQSr5taRMF+CmZ9ZDFqF2ewc9Pmw\nOMG4o/6m50Q2ELz23O8idjGxKgG7iGdEa3c5cQZTCQ0+2N77L+iS029AV9AKyZMi\nCwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"name": "Alice P. Hacker",
|
||||
"published": "2022-02-23T17:20:30+00:00",
|
||||
"updated": "2022-02-25T02:12:48+00:00",
|
||||
"url": "https://another_instance.gnusocial.test/@alice",
|
||||
"url": "https://another-instance.gnusocial.test/@alice",
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://another_instance.gnusocial.test/inbox.json"
|
||||
"sharedInbox": "https://another-instance.gnusocial.test/inbox.json"
|
||||
}
|
||||
}
|
@@ -25,7 +25,7 @@
|
||||
"id": "https://instance.gnusocial.test/object/note/1340",
|
||||
"published": "2022-03-16T21:54:43+00:00",
|
||||
"attributedTo": "https://instance.gnusocial.test/actor/42",
|
||||
"content": "<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another_instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>",
|
||||
"content": "<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another-instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>",
|
||||
"mediaType": "text/html",
|
||||
"source": {
|
||||
"content": "This is a public root note with a mention to @alice@another_instance.gnusocial.test.",
|
||||
@@ -35,7 +35,7 @@
|
||||
"tag": [
|
||||
{
|
||||
"type": "Mention",
|
||||
"href": "https://another_instance.gnusocial.test/actor/43",
|
||||
"href": "https://another-instance.gnusocial.test/actor/43",
|
||||
"name": "@alice@another_instance.gnusocial.test"
|
||||
}
|
||||
],
|
||||
|
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "https://gotosocial.org/users/the_mighty_zork",
|
||||
"id": "https://gotosocial.org/01E41WF691G30VVAV6TZXW10VT",
|
||||
"object": {
|
||||
"actor": "http://example.org/users/some_user",
|
||||
"id": "http://example.org/users/some_user/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3",
|
||||
"object": "https://gotosocial.org/users/the_mighty_zork",
|
||||
"to": "https://gotosocial.org/users/the_mighty_zork",
|
||||
"type": "Follow"
|
||||
},
|
||||
"to": "http://example.org/users/some_user",
|
||||
"type": "Accept"
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "https://example.org/users/the_mighty_zork",
|
||||
"cc": "https://example.org/users/the_mighty_zork",
|
||||
"id": "https://example.org/users/the_mighty_zork/statuses/01G74JJ1KS331G2JXHRMZCE0ER",
|
||||
"object": "https://example.org/users/the_mighty_zork/statuses/01FCTA44PW9H1TB328S9AQXKDS",
|
||||
"published": "2022-06-09T13:12:00Z",
|
||||
"to": "https://example.org/users/the_mighty_zork/followers",
|
||||
"type": "Announce"
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "https://example.org/users/admin",
|
||||
"cc": [
|
||||
"https://example.org/users/admin/followers",
|
||||
"https://example.org/users/the_mighty_zork"
|
||||
],
|
||||
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/activity",
|
||||
"object": {
|
||||
"attachment": [],
|
||||
"attributedTo": "https://example.org/users/admin",
|
||||
"cc": [
|
||||
"https://example.org/users/admin/followers",
|
||||
"https://example.org/users/the_mighty_zork"
|
||||
],
|
||||
"content": "hi @the_mighty_zork welcome to the instance!",
|
||||
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0",
|
||||
"inReplyTo": "https://example.org/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"published": "2021-11-20T13:32:16Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies?page=true",
|
||||
"next": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies?only_other_accounts=false\\u0026page=true",
|
||||
"partOf": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies",
|
||||
"type": "CollectionPage"
|
||||
},
|
||||
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies",
|
||||
"type": "Collection"
|
||||
},
|
||||
"sensitive": false,
|
||||
"summary": "",
|
||||
"tag": {
|
||||
"href": "https://example.org/users/the_mighty_zork",
|
||||
"name": "@the_mighty_zork@localhost:8080",
|
||||
"type": "Mention"
|
||||
},
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"url": "https://example.org/@admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0"
|
||||
},
|
||||
"published": "2021-11-20T13:32:16Z",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Create"
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "https://gotosocial.org/users/the_mighty_zork",
|
||||
"id": "https://gotosocial.org/users/the_mighty_zork/follow/01F8PY8RHWRQZV038T4E8T9YK8",
|
||||
"object": "https://example.com/users/admin",
|
||||
"to": "https://example.org/users/admin",
|
||||
"type": "Follow"
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "https://gotosocial.org/users/the_mighty_zork",
|
||||
"id": "https://gotosocial.org/01WKYFGS71GG2SXJ8T4FG9VRN2",
|
||||
"object": {
|
||||
"actor": "http://example.org/users/some_user",
|
||||
"id": "http://example.org/users/some_user/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3",
|
||||
"object": "https://gotosocial.org/users/the_mighty_zork",
|
||||
"to": "https://gotosocial.org/users/the_mighty_zork",
|
||||
"type": "Follow"
|
||||
},
|
||||
"to": "http://example.org/users/some_user",
|
||||
"type": "Reject"
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"http://joinmastodon.org/ns"
|
||||
],
|
||||
"actor": "https://example.org/users/the_mighty_zork",
|
||||
"bcc": "https://example.org/users/the_mighty_zork/followers",
|
||||
"id": "https://example.org/users/the_mighty_zork#updates/011HHHD988G37MD88E1YAF03E4",
|
||||
"object": {
|
||||
"discoverable": true,
|
||||
"featured": "https://example.org/users/the_mighty_zork/collections/featured",
|
||||
"followers": "https://example.org/users/the_mighty_zork/followers",
|
||||
"following": "https://example.org/users/the_mighty_zork/following",
|
||||
"icon": {
|
||||
"mediaType": "image/jpeg",
|
||||
"type": "Image",
|
||||
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"
|
||||
},
|
||||
"id": "https://example.org/users/the_mighty_zork",
|
||||
"image": {
|
||||
"mediaType": "image/jpeg",
|
||||
"type": "Image",
|
||||
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"
|
||||
},
|
||||
"inbox": "https://example.org/users/the_mighty_zork/inbox",
|
||||
"manuallyApprovesFollowers": false,
|
||||
"name": "original zork (he/they)",
|
||||
"outbox": "https://example.org/users/the_mighty_zork/outbox",
|
||||
"preferredUsername": "the_mighty_zork",
|
||||
"publicKey": {
|
||||
"id": "https://example.org/users/the_mighty_zork/main-key",
|
||||
"owner": "https://example.org/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"summary": "\\u003cp\\u003ehey yo this is my profile!\\u003c/p\\u003e",
|
||||
"tag": {
|
||||
"icon": {
|
||||
"mediaType": "image/png",
|
||||
"type": "Image",
|
||||
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"
|
||||
},
|
||||
"id": "https://example.org/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||
"name": ":rainbow:",
|
||||
"type": "Emoji",
|
||||
"updated": "2021-09-20T12:40:37+02:00"
|
||||
},
|
||||
"type": "Person",
|
||||
"url": "https://example.org/@the_mighty_zork"
|
||||
},
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Update"
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"http://joinmastodon.org/ns"
|
||||
],
|
||||
"attachment": {
|
||||
"blurhash": "LNJRdVM{00Rj%Mayt7j[4nWBofRj",
|
||||
"mediaType": "image/jpeg",
|
||||
"name": "Black and white image of some 50's style text saying: Welcome On Board",
|
||||
"type": "Document",
|
||||
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg"
|
||||
},
|
||||
"attributedTo": "https://example.org/users/admin",
|
||||
"cc": "https://example.org/users/admin/followers",
|
||||
"content": "hello world! #welcome ! first post on the instance :rainbow: !",
|
||||
"id": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"published": "2021-10-20T11:36:45Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
"id": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies?page=true",
|
||||
"next": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies?only_other_accounts=false\\u0026page=true",
|
||||
"partOf": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies",
|
||||
"type": "CollectionPage"
|
||||
},
|
||||
"id": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies",
|
||||
"type": "Collection"
|
||||
},
|
||||
"sensitive": false,
|
||||
"summary": "",
|
||||
"tag": {
|
||||
"icon": {
|
||||
"mediaType": "image/png",
|
||||
"type": "Image",
|
||||
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"
|
||||
},
|
||||
"id": "https://example.org/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||
"name": ":rainbow:",
|
||||
"type": "Emoji",
|
||||
"updated": "2021-09-20T10:40:37Z"
|
||||
},
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"url": "https://example.org/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R"
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"http://joinmastodon.org/ns",
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
"discoverable": true,
|
||||
"featured": "https://example.org/users/the_mighty_zork/collections/featured",
|
||||
"followers": "https://example.org/users/the_mighty_zork/followers",
|
||||
"following": "https://example.org/users/the_mighty_zork/following",
|
||||
"icon": {
|
||||
"mediaType": "image/jpeg",
|
||||
"type": "Image",
|
||||
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"
|
||||
},
|
||||
"id": "https://example.org/users/the_mighty_zork",
|
||||
"image": {
|
||||
"mediaType": "image/jpeg",
|
||||
"type": "Image",
|
||||
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"
|
||||
},
|
||||
"inbox": "https://example.org/users/the_mighty_zork/inbox",
|
||||
"manuallyApprovesFollowers": false,
|
||||
"name": "original zork (he/they)",
|
||||
"outbox": "https://example.org/users/the_mighty_zork/outbox",
|
||||
"preferredUsername": "the_mighty_zork",
|
||||
"publicKey": {
|
||||
"id": "https://example.org/users/the_mighty_zork/main-key",
|
||||
"owner": "https://example.org/users/the_mighty_zork",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"summary": "<p>hey yo this is my profile!</p>",
|
||||
"tag": {
|
||||
"icon": {
|
||||
"mediaType": "image/png",
|
||||
"type": "Image",
|
||||
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"
|
||||
},
|
||||
"id": "https://example.org/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||
"name": ":rainbow:",
|
||||
"type": "Emoji",
|
||||
"updated": "2021-09-20T12:40:37+02:00"
|
||||
},
|
||||
"type": "Person",
|
||||
"url": "https://example.org/@the_mighty_zork"
|
||||
}
|
@@ -68,7 +68,7 @@ class GSObjectNoteTest extends GNUsocialTestCase
|
||||
self::bootKernel();
|
||||
|
||||
$actor_uri = 'https://instance.gnusocial.test/actor/42';
|
||||
$another_actor_uri = 'https://another_instance.gnusocial.test/actor/43';
|
||||
$another_actor_uri = 'https://another-instance.gnusocial.test/actor/43';
|
||||
$object_uri = 'https://instance.gnusocial.test/object/note/1340';
|
||||
$note = ActivityPub::getObjectByUri($object_uri, try_online: false);
|
||||
static::assertInstanceOf(Note::class, $note);
|
||||
@@ -77,7 +77,7 @@ class GSObjectNoteTest extends GNUsocialTestCase
|
||||
static::assertSame('text/plain', $note->getContentType());
|
||||
static::assertSame('This is a public root note with a mention to @alice@another_instance.gnusocial.test.', $note->getContent());
|
||||
// TODO: implement proper sanitization rules
|
||||
//static::assertSame('<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another_instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>', $note->getRendered());
|
||||
//static::assertSame('<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another-instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>', $note->getRendered());
|
||||
static::assertSame('ActivityPub', $note->getSource());
|
||||
static::assertNull($note->getReplyTo());
|
||||
static::assertFalse($note->getIsLocal());
|
||||
|
@@ -37,6 +37,7 @@ use App\Core\HTTPClient;
|
||||
use App\Core\Log;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\NoSuchActorException;
|
||||
use App\Util\Nickname;
|
||||
@@ -49,6 +50,7 @@ use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* ActivityPub's own Explorer
|
||||
@@ -66,6 +68,8 @@ class Explorer
|
||||
* Shortcut function to get a single profile from its URL.
|
||||
*
|
||||
* @param bool $try_online whether to try online grabbing, defaults to true
|
||||
* @param Actor $on_behalf_of AP Actor on behalf of whom any remote lookups are to be performed, defaults to null.
|
||||
* If null, outgoing GET request(s) will not be http signed.
|
||||
*
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws NoSuchActorException
|
||||
@@ -73,9 +77,9 @@ class Explorer
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public static function getOneFromUri(string $uri, bool $try_online = true): Actor
|
||||
public static function getOneFromUri(string $uri, bool $try_online = true, ?Actor $on_behalf_of = null): Actor
|
||||
{
|
||||
$actors = (new self())->lookup($uri, $try_online);
|
||||
$actors = (new self())->lookup($uri, $try_online, $on_behalf_of);
|
||||
switch (\count($actors)) {
|
||||
case 1:
|
||||
return $actors[0];
|
||||
@@ -93,6 +97,8 @@ class Explorer
|
||||
*
|
||||
* @param string $uri User's url
|
||||
* @param bool $try_online whether to try online grabbing, defaults to true
|
||||
* @param Actor $on_behalf_of AP Actor on behalf of whom the lookup is being performed, defaults to null.
|
||||
* If null, outgoing GET request(s) will not be http signed.
|
||||
*
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws NoSuchActorException
|
||||
@@ -102,7 +108,7 @@ class Explorer
|
||||
*
|
||||
* @return array of Actor objects
|
||||
*/
|
||||
public function lookup(string $uri, bool $try_online = true): array
|
||||
public function lookup(string $uri, bool $try_online = true, ?Actor $on_behalf_of = null): array
|
||||
{
|
||||
if (\in_array($uri, ActivityPub::PUBLIC_TO)) {
|
||||
return [];
|
||||
@@ -111,7 +117,7 @@ class Explorer
|
||||
Log::debug('ActivityPub Explorer: Started now looking for ' . $uri);
|
||||
$this->discovered_actors = [];
|
||||
|
||||
return $this->_lookup($uri, $try_online);
|
||||
return $this->_lookup($uri, $try_online, $on_behalf_of);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,6 +127,8 @@ class Explorer
|
||||
*
|
||||
* @param string $uri User's url
|
||||
* @param bool $try_online whether to try online grabbing, defaults to true
|
||||
* @param Actor $on_behalf_of Actor on behalf of whom the lookup is being performed, defaults to null.
|
||||
* If null, outgoing GET request(s) will not be http signed.
|
||||
*
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws NoSuchActorException
|
||||
@@ -130,13 +138,13 @@ class Explorer
|
||||
*
|
||||
* @return array of Actor objects
|
||||
*/
|
||||
private function _lookup(string $uri, bool $try_online = true): array
|
||||
private function _lookup(string $uri, bool $try_online = true, ?Actor $on_behalf_of = null): array
|
||||
{
|
||||
$grab_known = $this->grabKnownActor($uri);
|
||||
|
||||
// First check if we already have it locally and, if so, return it.
|
||||
// If the known fetch fails and remote grab is required: store locally and return.
|
||||
if (!$grab_known && (!$try_online || !$this->grabRemoteActor($uri))) {
|
||||
if (!$grab_known && (!$try_online || !$this->grabRemoteActor($uri, $on_behalf_of))) {
|
||||
throw new NoSuchActorException('Actor not found.');
|
||||
}
|
||||
|
||||
@@ -158,32 +166,25 @@ class Explorer
|
||||
{
|
||||
Log::debug('ActivityPub Explorer: Searching locally for ' . $uri . ' offline.');
|
||||
|
||||
// Try local
|
||||
if (Common::isValidHttpUrl($uri)) {
|
||||
// This means $uri is a valid url
|
||||
$resource_parts = parse_url($uri);
|
||||
// TODO: Use URLMatcher
|
||||
if ($resource_parts['host'] === Common::config('site', 'server')) {
|
||||
$str = $resource_parts['path'];
|
||||
// actor_view_nickname
|
||||
$renick = '/\/@(' . Nickname::DISPLAY_FMT . ')\/?/m';
|
||||
// actor_view_id
|
||||
$reuri = '/\/actor\/(\d+)\/?/m';
|
||||
if (preg_match_all($renick, $str, $matches, \PREG_SET_ORDER, 0) === 1) {
|
||||
$this->discovered_actors[] = DB::findOneBy(
|
||||
LocalUser::class,
|
||||
['nickname' => $matches[0][1]],
|
||||
)->getActor();
|
||||
return true;
|
||||
} elseif (preg_match_all($reuri, $str, $matches, \PREG_SET_ORDER, 0) === 1) {
|
||||
$this->discovered_actors[] = Actor::getById((int) $matches[0][1]);
|
||||
return true;
|
||||
if (!Common::isValidHttpUrl($uri)) {
|
||||
Log::debug('ActivityPub Explorer: URI ' . $uri . ' was not a valid http url.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if uri corresponds to local actor
|
||||
$resource_parts = parse_url($uri);
|
||||
if ($resource_parts['host'] === Common::config('site', 'server')) {
|
||||
$actor = $this::getLocalActorForPath($resource_parts['path']);
|
||||
if (!\is_null($actor)) {
|
||||
Log::debug('ActivityPub Explorer: Found local ActivityPub Actor for ' . $uri);
|
||||
$this->discovered_actors[] = $actor;
|
||||
return true;
|
||||
} else {
|
||||
Log::debug('ActivityPub Explorer: Unable to find a known local ActivityPub Actor for ' . $uri);
|
||||
}
|
||||
}
|
||||
|
||||
// Try standard ActivityPub route
|
||||
// Is this a known filthy little mudblood?
|
||||
// URI isn't for a local actor, try to get by URI more generally
|
||||
$aprofile = DB::findOneBy(ActivitypubActor::class, ['uri' => $uri], return_null: true);
|
||||
if (!\is_null($aprofile)) {
|
||||
Log::debug('ActivityPub Explorer: Found a known ActivityPub Actor for ' . $uri);
|
||||
@@ -201,6 +202,8 @@ class Explorer
|
||||
* $this->discovered_actor_profiles
|
||||
*
|
||||
* @param string $uri User's url
|
||||
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
|
||||
* If null, outgoing GET request(s) will not be http signed.
|
||||
*
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws NoSuchActorException
|
||||
@@ -210,10 +213,9 @@ class Explorer
|
||||
*
|
||||
* @return bool success state
|
||||
*/
|
||||
private function grabRemoteActor(string $uri): bool
|
||||
private function grabRemoteActor(string $uri, ?Actor $on_behalf_of = null): bool
|
||||
{
|
||||
Log::debug('ActivityPub Explorer: Trying to grab a remote actor for ' . $uri);
|
||||
$response = HTTPClient::get($uri, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
|
||||
$response = $this->get($uri, $on_behalf_of);
|
||||
$res = json_decode($response->getContent(), true);
|
||||
if ($response->getStatusCode() == 410) { // If it was deleted
|
||||
return true; // Nothing to add.
|
||||
@@ -227,7 +229,7 @@ class Explorer
|
||||
|
||||
if ($res['type'] === 'OrderedCollection') { // It's a potential collection of actors!!!
|
||||
Log::debug('ActivityPub Explorer: Found a collection of actors for ' . $uri);
|
||||
$this->travelCollection($res['first']);
|
||||
$this->travelCollection($res['first'], $on_behalf_of);
|
||||
return true;
|
||||
} else {
|
||||
try {
|
||||
@@ -249,15 +251,19 @@ class Explorer
|
||||
/**
|
||||
* Allows the Explorer to transverse a collection of persons.
|
||||
*
|
||||
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
|
||||
* If null, outgoing GET request(s) will not be http signed.
|
||||
* @param string $uri Collection's url
|
||||
*
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws NoSuchActorException
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
private function travelCollection(string $uri): bool
|
||||
private function travelCollection(string $uri, ?Actor $on_behalf_of = null): bool
|
||||
{
|
||||
$response = HTTPClient::get($uri, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
|
||||
$response = $this->get($uri, $on_behalf_of);
|
||||
$res = json_decode($response->getContent(), true);
|
||||
|
||||
if (!isset($res['orderedItems'])) {
|
||||
@@ -266,22 +272,47 @@ class Explorer
|
||||
|
||||
// Accumulate findings
|
||||
foreach ($res['orderedItems'] as $actor_uri) {
|
||||
$this->_lookup($actor_uri);
|
||||
$this->_lookup($actor_uri, true, $on_behalf_of);
|
||||
}
|
||||
|
||||
// Go through entire collection
|
||||
if (!\is_null($res['next'])) {
|
||||
$this->travelCollection($res['next']);
|
||||
$this->travelCollection($res['next'], $on_behalf_of);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an http GET request to the given uri. Will be http-signed on behalf of given Actor, if provided.
|
||||
*
|
||||
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
|
||||
* If null, outgoing GET request(s) will not be http signed.
|
||||
* @param string $uri uri of remote resource, expected to return an Activity/Object of some kind.
|
||||
*
|
||||
* @return ResponseInterface The http response, for further processing.
|
||||
*/
|
||||
public static function get(string $uri, ?Actor $on_behalf_of = null): ResponseInterface
|
||||
{
|
||||
$headers = [];
|
||||
if (!\is_null($on_behalf_of)) {
|
||||
// sign the http GET request
|
||||
$headers = HTTPSignature::sign($on_behalf_of, $uri, body: false, addlHeaders: [], method: 'get');
|
||||
} else {
|
||||
// just do a bare request
|
||||
$headers = ACTIVITYPUB::HTTP_CLIENT_HEADERS;
|
||||
}
|
||||
|
||||
return HTTPClient::get($uri, ['headers' => $headers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a remote user array from its URL (this function is only used for
|
||||
* profile updating and shall not be used for anything else)
|
||||
*
|
||||
* @param string $uri User's url
|
||||
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
|
||||
* If null, outgoing GET request(s) will not be http signed.
|
||||
*
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws Exception
|
||||
@@ -292,9 +323,9 @@ class Explorer
|
||||
* @return null|string If it is able to fetch, false if it's gone
|
||||
* // Exceptions when network issues or unsupported Activity format
|
||||
*/
|
||||
public static function getRemoteActorActivity(string $uri): string|null
|
||||
public static function getRemoteActorActivity(string $uri, ?Actor $on_behalf_of = null): string|null
|
||||
{
|
||||
$response = HTTPClient::get($uri, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
|
||||
$response = Explorer::get($uri, $on_behalf_of);
|
||||
// If it was deleted
|
||||
if ($response->getStatusCode() == 410) {
|
||||
return null;
|
||||
@@ -303,4 +334,37 @@ class Explorer
|
||||
}
|
||||
return $response->getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given path and return the actor it corresponds to.
|
||||
*
|
||||
* @param String $path Path on *this instance*. Will be parsed with regular expressions.
|
||||
* Something like `/actor/1` or `/object/note/1`.
|
||||
*
|
||||
* @return Actor|null The actor corresponding to/owning the given uri, null if not found.
|
||||
*/
|
||||
public static function getLocalActorForPath(string $path): Actor|null
|
||||
{
|
||||
// TODO: Use URLMatcher
|
||||
|
||||
// actor_view_nickname
|
||||
$renick = '/\/@(' . Nickname::DISPLAY_FMT . ')\/?/';
|
||||
if (preg_match_all($renick, $path, $matches, \PREG_SET_ORDER, 0) === 1) {
|
||||
return DB::findOneBy(LocalUser::class, ['nickname' => $matches[0][1]])->getActor();
|
||||
}
|
||||
|
||||
// actor_view_id
|
||||
$reuri = '/\/actor\/(\d+)\/?/';
|
||||
if (preg_match_all($reuri, $path, $matches, \PREG_SET_ORDER, 0) === 1) {
|
||||
return Actor::getById((int) $matches[0][1]);
|
||||
}
|
||||
|
||||
// note / page / article match
|
||||
$renote = '/\/object\/(?:note|page|article)\/(\d+)\/?/';
|
||||
if (preg_match_all($renote, $path, $matches, \PREG_SET_ORDER, 0) === 1) {
|
||||
return Note::getById((int) $matches[0][1])->getActor();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -47,13 +47,13 @@ class HTTPSignature
|
||||
*
|
||||
* @return array Headers to be used in request
|
||||
*/
|
||||
public static function sign(Actor $user, string $url, string|bool $body = false, array $addlHeaders = []): array
|
||||
public static function sign(Actor $user, string $url, string|bool $body = false, array $addlHeaders = [], string $method = 'post'): array
|
||||
{
|
||||
$digest = false;
|
||||
if ($body) {
|
||||
$digest = self::_digest($body);
|
||||
}
|
||||
$headers = self::_headersToSign($url, $digest);
|
||||
$headers = self::_headersToSign($url, $digest, $method);
|
||||
$headers = array_merge($headers, $addlHeaders);
|
||||
$stringToSign = self::_headersToSigningString($headers);
|
||||
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
|
||||
@@ -81,14 +81,23 @@ class HTTPSignature
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a canonical array of http headers, ready to be signed.
|
||||
*
|
||||
* @param string $uri uri of destination
|
||||
* @param string|bool $digest digest of the request body to add to the `Digest` header (optional).
|
||||
* @param string $method http method (GET, POST, etc) that the request will use.
|
||||
* This will be used in the `(request-target)` part of the signature.
|
||||
*
|
||||
* @return array Headers to be signed.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected static function _headersToSign(string $url, string|bool $digest = false): array
|
||||
protected static function _headersToSign(string $url, string|bool $digest = false, string $method): array
|
||||
{
|
||||
$date = new DateTime('UTC');
|
||||
|
||||
$headers = [
|
||||
'(request-target)' => 'post ' . parse_url($url, \PHP_URL_PATH),
|
||||
'(request-target)' => strtolower($method) . ' ' . parse_url($url, \PHP_URL_PATH),
|
||||
'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
|
||||
'Host' => parse_url($url, \PHP_URL_HOST),
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, application/json',
|
||||
|
@@ -118,7 +118,7 @@ class Actor extends Model
|
||||
'modified' => new DateTime(),
|
||||
];
|
||||
if (isset($options['objects']['Actor'])) {
|
||||
$actor = GSActor::create($actor_map, $options['objects']['Actor']);
|
||||
$actor = GSActor::createOrUpdate($options['objects']['Actor'], $actor_map);
|
||||
} else {
|
||||
$actor = GSActor::create($actor_map);
|
||||
DB::persist($actor);
|
||||
@@ -133,7 +133,7 @@ class Actor extends Model
|
||||
'url' => $object->get('url') ?? null,
|
||||
];
|
||||
if (isset($options['objects']['ActivitypubActor'])) {
|
||||
$ap_actor = ActivitypubActor::create($ap_actor_map, $options['objects']['ActivitypubActor']);
|
||||
$ap_actor = ActivitypubActor::createOrUpdate($options['objects']['ActivitypubActor'], $ap_actor_map);
|
||||
} else {
|
||||
$ap_actor = ActivitypubActor::create($ap_actor_map);
|
||||
DB::persist($ap_actor);
|
||||
@@ -145,7 +145,7 @@ class Actor extends Model
|
||||
'public_key' => ($object->has('publicKey') && isset($object->get('publicKey')['publicKeyPem'])) ? $object->get('publicKey')['publicKeyPem'] : null,
|
||||
];
|
||||
if (isset($options['objects']['ActivitypubRsa'])) {
|
||||
$apRSA = ActivitypubRsa::create($ap_rsa_map, $options['objects']['ActivitypubRsa']);
|
||||
$apRSA = ActivitypubRsa::createOrUpdate($options['objects']['ActivitypubRsa'], $ap_rsa_map);
|
||||
} else {
|
||||
$apRSA = ActivitypubRsa::create($ap_rsa_map);
|
||||
DB::persist($apRSA);
|
||||
|
@@ -41,6 +41,7 @@ use App\Entity\Feed;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Nickname;
|
||||
use Component\Collection\Util\MetaCollectionTrait;
|
||||
use EventResult;
|
||||
use Plugin\AttachmentCollections\Controller\AttachmentCollections as AttachmentCollectionsController;
|
||||
use Plugin\AttachmentCollections\Entity\AttachmentCollection;
|
||||
use Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry;
|
||||
@@ -48,10 +49,15 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AttachmentCollections extends Plugin
|
||||
{
|
||||
/** @phpstan-use MetaCollectionTrait<AttachmentCollection> */
|
||||
use MetaCollectionTrait;
|
||||
protected const SLUG = 'collection';
|
||||
protected const PLURAL_SLUG = 'collections';
|
||||
protected function createCollection(Actor $owner, array $vars, string $name)
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
*/
|
||||
protected function createCollection(Actor $owner, array $vars, string $name): void
|
||||
{
|
||||
$col = AttachmentCollection::create([
|
||||
'name' => $name,
|
||||
@@ -64,7 +70,13 @@ class AttachmentCollections extends Plugin
|
||||
'attachment_collection_id' => $col->getId(),
|
||||
]));
|
||||
}
|
||||
protected function removeItem(Actor $owner, array $vars, array $items, array $collections)
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
* @param int[] $items
|
||||
* @param array<string, mixed> $collections
|
||||
*/
|
||||
protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool
|
||||
{
|
||||
return DB::dql(<<<'EOF'
|
||||
DELETE FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry
|
||||
@@ -82,7 +94,12 @@ class AttachmentCollections extends Plugin
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addItem(Actor $owner, array $vars, array $items, array $collections)
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
* @param int[] $items
|
||||
* @param array<string, mixed> $collections
|
||||
*/
|
||||
protected function addItem(Actor $owner, array $vars, array $items, array $collections): void
|
||||
{
|
||||
foreach ($items as $id) {
|
||||
// prevent user from putting something in a collection (s)he doesn't own:
|
||||
@@ -96,11 +113,19 @@ class AttachmentCollections extends Plugin
|
||||
}
|
||||
}
|
||||
|
||||
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
*/
|
||||
protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool
|
||||
{
|
||||
return $vars['path'] === 'note_attachment_show';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
*
|
||||
* @return ($ids_only is true ? int[] : AttachmentCollection[])
|
||||
*/
|
||||
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
|
||||
{
|
||||
if (\is_null($vars)) {
|
||||
@@ -122,7 +147,7 @@ class AttachmentCollections extends Plugin
|
||||
return array_map(fn ($x) => $x['attachment_collection_id'], $res);
|
||||
}
|
||||
|
||||
public function onAddRoute(Router $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
// View all collections by actor id and nickname
|
||||
$r->connect(
|
||||
@@ -149,7 +174,7 @@ class AttachmentCollections extends Plugin
|
||||
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(Feed::create([
|
||||
'actor_id' => $actor_id,
|
||||
|
@@ -28,15 +28,21 @@ use App\Core\Router;
|
||||
use Component\Collection\Util\Controller\MetaCollectionController;
|
||||
use Plugin\AttachmentCollections\Entity\AttachmentCollection;
|
||||
|
||||
/**
|
||||
* @extends MetaCollectionController<AttachmentCollection>
|
||||
*/
|
||||
class AttachmentCollections extends MetaCollectionController
|
||||
{
|
||||
public function createCollection(int $owner_id, string $name)
|
||||
public function createCollection(int $owner_id, string $name): bool
|
||||
{
|
||||
DB::persist(AttachmentCollection::create([
|
||||
'name' => $name,
|
||||
'actor_id' => $owner_id,
|
||||
]));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
|
||||
{
|
||||
if (\is_null($owner_nickname)) {
|
||||
@@ -50,7 +56,12 @@ class AttachmentCollections extends MetaCollectionController
|
||||
['nickname' => $owner_nickname, 'cid' => $collection_id],
|
||||
);
|
||||
}
|
||||
public function getCollectionItems(int $owner_id, $collection_id): array
|
||||
|
||||
/**
|
||||
* FIXME return value not consistent with base class
|
||||
*/
|
||||
// @phpstan-disable-next-line
|
||||
public function getCollectionItems(int $owner_id, int $collection_id): array
|
||||
{
|
||||
[$attachs, $notes] = DB::dql(
|
||||
<<<'EOF'
|
||||
@@ -63,28 +74,30 @@ class AttachmentCollections extends MetaCollectionController
|
||||
EOF,
|
||||
['cid' => $collection_id],
|
||||
);
|
||||
return [
|
||||
'_template' => 'AttachmentCollections/collection_entry_view.html.twig',
|
||||
'attachments' => array_values($attachs),
|
||||
'bare_notes' => array_values($notes),
|
||||
];
|
||||
|
||||
return ['_template' => 'AttachmentCollections/collection_entry_view.html.twig', 'attachments' => array_values($attachs), 'bare_notes' => array_values($notes)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AttachmentCollection[]
|
||||
*/
|
||||
public function getCollectionsByActorId(int $owner_id): array
|
||||
{
|
||||
return DB::findBy(AttachmentCollection::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']);
|
||||
}
|
||||
|
||||
public function getCollectionBy(int $owner_id, int $collection_id): AttachmentCollection
|
||||
{
|
||||
return DB::findOneBy(AttachmentCollection::class, ['id' => $collection_id]);
|
||||
}
|
||||
|
||||
public function setCollectionName(int $actor_id, string $actor_nickname, AttachmentCollection $collection, string $name)
|
||||
public function setCollectionName(int $actor_id, string $actor_nickname, AttachmentCollection $collection, string $name): void
|
||||
{
|
||||
$collection->setName($name);
|
||||
DB::persist($collection);
|
||||
}
|
||||
|
||||
public function removeCollection(int $actor_id, string $actor_nickname, AttachmentCollection $collection)
|
||||
public function removeCollection(int $actor_id, string $actor_nickname, AttachmentCollection $collection): void
|
||||
{
|
||||
DB::remove($collection);
|
||||
}
|
||||
|
@@ -28,11 +28,16 @@ use App\Core\Event;
|
||||
use App\Core\Modules\Plugin;
|
||||
use App\Util\Common;
|
||||
use App\Util\Formatting;
|
||||
use EventResult;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AttachmentShowRelated extends Plugin
|
||||
{
|
||||
public function onAppendRightPanelBlock(Request $request, $vars, &$res): bool
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
* @param string[] $res
|
||||
*/
|
||||
public function onAppendRightPanelBlock(Request $request, array $vars, array &$res): EventResult
|
||||
{
|
||||
if ($vars['path'] === 'note_attachment_show') {
|
||||
$related_notes = DB::dql('select n from attachment_to_note an '
|
||||
@@ -50,11 +55,9 @@ class AttachmentShowRelated extends Plugin
|
||||
/**
|
||||
* 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 $path): bool
|
||||
public function onEndShowStyles(array &$styles, string $path): EventResult
|
||||
{
|
||||
if ($path === 'note_attachment_show') {
|
||||
$styles[] = '/assets/default_theme/pages/feeds.css';
|
||||
|
@@ -38,6 +38,7 @@ use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Plugin;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Formatting;
|
||||
use EventResult;
|
||||
use FFMpeg\FFProbe as ffprobe;
|
||||
use SplFileInfo;
|
||||
|
||||
@@ -53,7 +54,10 @@ class AudioEncoder extends Plugin
|
||||
return GSFile::mimetypeMajor($mimetype) === 'audio';
|
||||
}
|
||||
|
||||
public function onFileMetaAvailable(array &$event_map, string $mimetype): bool
|
||||
/**
|
||||
* @param array<string, callable> $event_map
|
||||
*/
|
||||
public function onFileMetaAvailable(array &$event_map, string $mimetype): EventResult
|
||||
{
|
||||
if (!self::shouldHandle($mimetype)) {
|
||||
return Event::next;
|
||||
@@ -89,8 +93,11 @@ class AudioEncoder extends Plugin
|
||||
|
||||
/**
|
||||
* Generates the view for attachments of type Video
|
||||
*
|
||||
* @param (array{attachment: \Component\Attachment\Entity\Attachment, note: \App\Entity\Note, title: string} & array<string, mixed>) $vars
|
||||
* @param array<string> $res
|
||||
*/
|
||||
public function onViewAttachment(array $vars, array &$res): bool
|
||||
public function onViewAttachment(array $vars, array &$res): EventResult
|
||||
{
|
||||
if (!self::shouldHandle($vars['attachment']->getMimetype())) {
|
||||
return Event::next;
|
||||
@@ -108,9 +115,11 @@ class AudioEncoder extends Plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ModuleVersionType[] $versions
|
||||
*
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onPluginVersion(array &$versions): bool
|
||||
public function onPluginVersion(array &$versions): EventResult
|
||||
{
|
||||
$versions[] = [
|
||||
'name' => 'AudioEncoder',
|
||||
|
@@ -23,22 +23,27 @@ declare(strict_types = 1);
|
||||
namespace Plugin\Blog;
|
||||
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Plugin;
|
||||
use App\Core\Router;
|
||||
use App\Util\Common;
|
||||
use App\Util\HTML;
|
||||
use EventResult;
|
||||
use Plugin\Blog\Controller as C;
|
||||
use function App\Core\I18n\_m;
|
||||
|
||||
class Blog extends Plugin
|
||||
{
|
||||
public function onAddRoute(Router $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect(id: 'blog_post', uri_path: '/blog/post', target: [C\Post::class, 'makePost']);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onAppendCardProfile(array $vars, array &$res): bool
|
||||
/**
|
||||
* @param (array{actor: \App\Entity\Actor} & array<string, mixed>) $vars
|
||||
* @param array<string> $res
|
||||
*/
|
||||
public function onAppendCardProfile(array $vars, array &$res): EventResult
|
||||
{
|
||||
$actor = Common::actor();
|
||||
$group = $vars['actor'];
|
||||
|
@@ -32,11 +32,17 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Bundles extends Plugin
|
||||
{
|
||||
/**
|
||||
* @phpstan-use MetaCollectionTrait<BundleCollection>
|
||||
*/
|
||||
use MetaCollectionTrait;
|
||||
protected const SLUG = 'bundle';
|
||||
protected const PLURAL_SLUG = 'bundles';
|
||||
|
||||
protected function createCollection(Actor $owner, array $vars, string $name)
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
*/
|
||||
protected function createCollection(Actor $owner, array $vars, string $name): void
|
||||
{
|
||||
$column = BundleCollection::create([
|
||||
'name' => $name,
|
||||
@@ -49,7 +55,12 @@ class Bundles extends Plugin
|
||||
]));
|
||||
}
|
||||
|
||||
protected function removeItem(Actor $owner, array $vars, array $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
|
||||
{
|
||||
return DB::dql(<<<'EOF'
|
||||
DELETE FROM \Plugin\BlogCollections\Entity\BlogCollectionEntry AS entry
|
||||
@@ -66,7 +77,12 @@ class Bundles extends Plugin
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addItem(Actor $owner, array $vars, array $items, array $collections)
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
* @param array<int> $items
|
||||
* @param array<int> $collections
|
||||
*/
|
||||
protected function addItem(Actor $owner, array $vars, array $items, array $collections): void
|
||||
{
|
||||
foreach ($items as $id) {
|
||||
// prevent user from putting something in a collection (s)he doesn't own:
|
||||
@@ -79,12 +95,22 @@ class Bundles extends Plugin
|
||||
}
|
||||
}
|
||||
|
||||
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool
|
||||
/**
|
||||
* @param array<string, mixed> $vars
|
||||
*/
|
||||
protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool
|
||||
{
|
||||
// TODO: Implement shouldAddToRightPanel() method.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME incompatible return type
|
||||
*
|
||||
* @param null|array<string, mixed> $vars
|
||||
*
|
||||
* @return BundleCollection[]|int[]
|
||||
*/
|
||||
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
|
||||
{
|
||||
if (\is_null($vars)) {
|
||||
|
@@ -26,11 +26,14 @@ namespace Plugin\Bundles\Controller;
|
||||
use App\Core\DB;
|
||||
use App\Core\Router;
|
||||
use Component\Collection\Util\Controller\MetaCollectionController;
|
||||
use Plugin\Bundles\Entity\BundleCollection;
|
||||
use Plugin\Bundles\Entity\BundleCollection as BundleCollectionEntity;
|
||||
|
||||
class BundleCollections extends MetaCollectionController
|
||||
/**
|
||||
* @extends MetaCollectionController<BundleCollectionEntity>
|
||||
*/
|
||||
class BundleCollection extends MetaCollectionController
|
||||
{
|
||||
public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string
|
||||
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
|
||||
{
|
||||
if (\is_null($owner_nickname)) {
|
||||
return Router::url(
|
||||
@@ -44,7 +47,8 @@ class BundleCollections extends MetaCollectionController
|
||||
);
|
||||
}
|
||||
|
||||
public function getCollectionItems(int $owner_id, $collection_id): array
|
||||
// FIXME
|
||||
public function getCollectionItems(int $owner_id, int $collection_id): array
|
||||
{
|
||||
[$notes] = DB::dql(
|
||||
<<<'EOF'
|
||||
@@ -65,19 +69,21 @@ class BundleCollections extends MetaCollectionController
|
||||
|
||||
public function getCollectionsByActorId(int $owner_id): array
|
||||
{
|
||||
return DB::findBy(BundleCollection::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']);
|
||||
return DB::findBy(BundleCollectionEntity::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']);
|
||||
}
|
||||
|
||||
public function getCollectionBy(int $owner_id, int $collection_id)
|
||||
public function getCollectionBy(int $owner_id, int $collection_id): BundleCollectionEntity
|
||||
{
|
||||
return DB::findOneBy(BundleCollection::class, ['id' => $collection_id]);
|
||||
return DB::findOneBy(BundleCollectionEntity::class, ['id' => $collection_id]);
|
||||
}
|
||||
|
||||
public function createCollection(int $owner_id, string $name)
|
||||
public function createCollection(int $owner_id, string $name): bool
|
||||
{
|
||||
DB::persist(BundleCollection::create([
|
||||
DB::persist(BundleCollectionEntity::create([
|
||||
'name' => $name,
|
||||
'actor_id' => $owner_id,
|
||||
]));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -58,7 +58,7 @@ class Cover
|
||||
* @throws ClientException Invalid form
|
||||
* @throws ServerException Invalid file type
|
||||
*
|
||||
* @return array template
|
||||
* @return ControllerResultType
|
||||
*/
|
||||
public static function coverSettings(Request $request): array
|
||||
{
|
||||
@@ -112,7 +112,9 @@ class Cover
|
||||
DB::flush();
|
||||
// Only delete files if the commit went through
|
||||
if ($old_file != null) {
|
||||
@unlink($old_file);
|
||||
foreach ($old_file as $f) {
|
||||
@unlink($f->getPath());
|
||||
}
|
||||
}
|
||||
throw new RedirectException();
|
||||
}
|
||||
@@ -128,7 +130,9 @@ class Cover
|
||||
$old_file = $cover->delete();
|
||||
DB::remove($cover);
|
||||
DB::flush();
|
||||
@unlink($old_file);
|
||||
foreach ($old_file as $f) {
|
||||
@unlink($f->getPath());
|
||||
}
|
||||
throw new RedirectException();
|
||||
}
|
||||
$removeForm = $form2->createView();
|
||||
|
@@ -27,6 +27,7 @@ use App\Core\Event;
|
||||
use App\Core\Modules\Plugin;
|
||||
use App\Core\Router;
|
||||
use App\Util\Common;
|
||||
use EventResult;
|
||||
use Plugin\Cover\Controller as C;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -44,17 +45,18 @@ class Cover extends Plugin
|
||||
{
|
||||
/**
|
||||
* Map URLs to actions
|
||||
*
|
||||
* @return bool hook value; true means continue processing, false means stop
|
||||
*/
|
||||
public function onAddRoute(Router $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('settings_profile_cover', 'settings/cover', [Controller\Cover::class, 'coversettings']);
|
||||
$r->connect('cover', '/cover', [Controller\Cover::class, 'cover']);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs)
|
||||
/**
|
||||
* @param SettingsTabsType $tabs
|
||||
*/
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): EventResult
|
||||
{
|
||||
if ($section === 'profile') {
|
||||
$tabs[] = [
|
||||
@@ -72,7 +74,7 @@ class Cover extends Plugin
|
||||
*
|
||||
* @return bool hook value; true means continue processing, false means stop.
|
||||
*
|
||||
* public function onStartTwigPopulateVars(array &$vars): bool
|
||||
* public function onStartTwigPopulateVars(array &$vars): \EventResult
|
||||
* {
|
||||
* if (Common::user() != null) {
|
||||
* $cover = DB::find('cover', ['actor_id' => Common::user()->getId()]);
|
||||
@@ -88,11 +90,9 @@ class Cover extends Plugin
|
||||
/**
|
||||
* 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 onStartShowStyles(array &$styles): bool
|
||||
public function onStartShowStyles(array &$styles): EventResult
|
||||
{
|
||||
$styles[] = 'assets/css/cover.css';
|
||||
return Event::next;
|
||||
|
@@ -117,7 +117,7 @@ class Cover extends Entity
|
||||
/**
|
||||
* Delete this cover and the corresponding attachment and thumbnails, which this owns
|
||||
*
|
||||
* @return array attachments deleted (if delete_attachments_now is true)
|
||||
* @return Attachment[] attachments deleted (if delete_attachments_now is true)
|
||||
*/
|
||||
public function delete(bool $flush = false, bool $delete_attachments_now = false, bool $cascading = false): array
|
||||
{
|
||||
|
@@ -34,6 +34,7 @@ use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use DateTime;
|
||||
use EventResult;
|
||||
use Plugin\ActivityPub\Entity\ActivitypubActivity;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -132,10 +133,8 @@ class DeleteNote extends NoteHandlerPlugin
|
||||
/**
|
||||
* Adds and connects the _delete_note_action_ route to
|
||||
* Controller\DeleteNote::class
|
||||
*
|
||||
* @return bool Event hook
|
||||
*/
|
||||
public function onAddRoute(Router $r)
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect(id: 'delete_note_action', uri_path: '/object/note/{note_id<\d+>}/delete', target: Controller\DeleteNote::class);
|
||||
|
||||
@@ -152,9 +151,9 @@ class DeleteNote extends NoteHandlerPlugin
|
||||
* @throws \App\Util\Exception\NotFoundException
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
*
|
||||
* @return bool Event hook
|
||||
* @params string[] $actions
|
||||
*/
|
||||
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions)
|
||||
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): EventResult
|
||||
{
|
||||
if (\is_null($actor = Common::actor())) {
|
||||
return Event::next;
|
||||
@@ -192,10 +191,8 @@ class DeleteNote extends NoteHandlerPlugin
|
||||
* @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
|
||||
* @param mixed $type_object Activity's Object
|
||||
* @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
private function activitypub_handler(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): bool
|
||||
private function activitypub_handler(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): EventResult
|
||||
{
|
||||
if ($type_activity->get('type') !== 'Delete'
|
||||
|| !($type_object instanceof Note)) {
|
||||
@@ -224,10 +221,8 @@ class DeleteNote extends NoteHandlerPlugin
|
||||
* @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
|
||||
* @param \ActivityPhp\Type\AbstractObject $type_object Activity Streams 2.0 Object
|
||||
* @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
public function onNewActivityPubActivity(Actor $actor, AbstractObject $type_activity, AbstractObject $type_object, ?ActivitypubActivity &$ap_act): bool
|
||||
public function onNewActivityPubActivity(Actor $actor, AbstractObject $type_activity, AbstractObject $type_object, ?ActivitypubActivity &$ap_act): EventResult
|
||||
{
|
||||
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
|
||||
}
|
||||
@@ -239,10 +234,8 @@ class DeleteNote extends NoteHandlerPlugin
|
||||
* @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
|
||||
* @param mixed $type_object Object
|
||||
* @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
public function onNewActivityPubActivityWithObject(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): bool
|
||||
public function onNewActivityPubActivityWithObject(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): EventResult
|
||||
{
|
||||
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
|
||||
}
|
||||
@@ -252,10 +245,8 @@ class DeleteNote extends NoteHandlerPlugin
|
||||
*
|
||||
* @param string $verb GNU social's internal verb
|
||||
* @param null|string $gs_verb_to_activity_stream_two_verb Resulting Activity Streams 2.0 verb
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): bool
|
||||
public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): EventResult
|
||||
{
|
||||
if ($verb === 'delete') {
|
||||
$gs_verb_to_activity_stream_two_verb = 'Delete';
|
||||
|
@@ -31,16 +31,15 @@ use App\Util\Exception\RedirectException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Formatting;
|
||||
use Component\Group\Controller as ComponentGroupController;
|
||||
use EventResult;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Directory extends Plugin
|
||||
{
|
||||
/**
|
||||
* Map Directory routes to its corresponding Controllers
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onAddRoute(Router $r)
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('directory_people', '/directory/people', [Controller\Directory::class, 'people']);
|
||||
$r->connect('directory_groups', '/directory/groups', [Controller\Directory::class, 'groups']);
|
||||
@@ -52,10 +51,8 @@ class Directory extends Plugin
|
||||
* Add Links to main navigation card
|
||||
*
|
||||
* @param array $res out menu items
|
||||
*
|
||||
* @return bool hook value; true means continue processing, false means stop
|
||||
*/
|
||||
public function onAddMainNavigationItem(array $vars, array &$res): bool
|
||||
public function onAddMainNavigationItem(array $vars, array &$res): EventResult
|
||||
{
|
||||
$res[] = ['title' => 'People', 'path' => Router::url($path_id = 'directory_people', []), 'path_id' => $path_id];
|
||||
$res[] = ['title' => 'Groups', 'path' => Router::url($path_id = 'directory_groups', []), 'path_id' => $path_id];
|
||||
@@ -69,10 +66,8 @@ class Directory extends Plugin
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onPrependActorsCollection(Request $request, array &$elements): bool
|
||||
public function onPrependActorsCollection(Request $request, array &$elements): EventResult
|
||||
{
|
||||
if (\is_null($actor = Common::actor())) {
|
||||
return Event::next;
|
||||
|
@@ -36,10 +36,11 @@ namespace Plugin\EmailNotifications;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Plugin;
|
||||
use EventResult;
|
||||
|
||||
class EmailNotifications extends Plugin
|
||||
{
|
||||
public function onAddNotificationTransport(&$form_defs): bool
|
||||
public function onAddNotificationTransport(array &$form_defs): EventResult
|
||||
{
|
||||
$form_defs['Email'] = $form_defs['placeholder'];
|
||||
$form_defs['Email'][] = $form_defs['placeholder']['save']('Email', 'save_email');
|
||||
|
@@ -216,7 +216,7 @@ class OEmbed extends Controller
|
||||
/**
|
||||
* Placeholder
|
||||
*/
|
||||
public function init_document($type)
|
||||
public function init_document(string $type): void
|
||||
{
|
||||
throw new NotImplementedException;
|
||||
// switch ($type) {
|
||||
@@ -243,7 +243,7 @@ class OEmbed extends Controller
|
||||
/**
|
||||
* Placeholder
|
||||
*/
|
||||
public function end_document($type)
|
||||
public function end_document(string $type): void
|
||||
{
|
||||
throw new NotImplementedException;
|
||||
// switch ($type) {
|
||||
|
@@ -57,6 +57,7 @@ use App\Util\TemporaryFile;
|
||||
use Component\Attachment\Entity\Attachment;
|
||||
use Component\Link\Entity\Link;
|
||||
use Embed\Embed as LibEmbed;
|
||||
use EventResult;
|
||||
use Exception;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
@@ -111,7 +112,7 @@ class Embed extends Plugin
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onAddRoute(Router $m): bool
|
||||
public function onAddRoute(Router $m): EventResult
|
||||
{
|
||||
$m->connect('oembed', 'main/oembed', Controller\OEmbed::class);
|
||||
return Event::next;
|
||||
@@ -120,7 +121,7 @@ class Embed extends Plugin
|
||||
/**
|
||||
* Insert oembed and opengraph tags in all HTML head elements
|
||||
*/
|
||||
public function onShowHeadElements(Request $request, array &$result): bool
|
||||
public function onShowHeadElements(Request $request, array &$result): EventResult
|
||||
{
|
||||
$matches = [];
|
||||
preg_match(',/?([^/]+)/?(.*),', $request->getPathInfo(), $matches);
|
||||
@@ -146,7 +147,7 @@ class Embed extends Plugin
|
||||
/**
|
||||
* Show this attachment enhanced with the corresponding Embed data, if available
|
||||
*/
|
||||
public function onViewLink(array $vars, array &$res): bool
|
||||
public function onViewLink(array $vars, array &$res): EventResult
|
||||
{
|
||||
$link = $vars['link'];
|
||||
try {
|
||||
@@ -177,7 +178,7 @@ class Embed extends Plugin
|
||||
*
|
||||
* @throws DuplicateFoundException
|
||||
*/
|
||||
public function onNewLinkFromNote(Link $link, Note $note): bool
|
||||
public function onNewLinkFromNote(Link $link, Note $note): EventResult
|
||||
{
|
||||
// Only handle text mime
|
||||
$mimetype = $link->getMimetype();
|
||||
@@ -368,7 +369,7 @@ class Embed extends Plugin
|
||||
return HTTPClient::get($url)->getContent();
|
||||
}
|
||||
|
||||
public function onAttachmentGetBestTitle(Attachment $attachment, Note $note, ?string &$title)
|
||||
public function onAttachmentGetBestTitle(Attachment $attachment, Note $note, ?string &$title): EventResult
|
||||
{
|
||||
try {
|
||||
$embed = DB::findOneBy('attachment_embed', ['attachment_id' => $attachment->getId()]);
|
||||
@@ -386,10 +387,8 @@ class Embed extends Plugin
|
||||
* @param array $versions inherited from parent
|
||||
*
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return bool true hook value
|
||||
*/
|
||||
public function onPluginVersion(array &$versions): bool
|
||||
public function onPluginVersion(array &$versions): EventResult
|
||||
{
|
||||
$versions[] = [
|
||||
'name' => 'Embed',
|
||||
|
@@ -43,6 +43,7 @@ final class EmbedTest extends TestCase
|
||||
*/
|
||||
public function testEmbed(string $url, string $expectedType)
|
||||
{
|
||||
static::markTestIncomplete();
|
||||
// try {
|
||||
// $data = EmbedHelper::getObject($url);
|
||||
// static::assertSame($expectedType, $data->type);
|
||||
|
@@ -26,6 +26,7 @@ namespace Plugin\Favourite\Controller;
|
||||
use App\Core\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router;
|
||||
use App\Entity\Activity;
|
||||
@@ -41,10 +42,12 @@ use App\Util\Exception\RedirectException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Component\Notification\Entity\Attention;
|
||||
use function App\Core\I18n\_m;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* @extends FeedController<Note>
|
||||
*/
|
||||
class Favourite extends FeedController
|
||||
{
|
||||
/**
|
||||
|
@@ -45,6 +45,7 @@ use App\Util\Common;
|
||||
use App\Util\Nickname;
|
||||
use Component\Notification\Entity\Attention;
|
||||
use DateTime;
|
||||
use EventResult;
|
||||
use Plugin\Favourite\Entity\NoteFavourite;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -122,10 +123,8 @@ class Favourite extends NoteHandlerPlugin
|
||||
*
|
||||
* @param Note $note Current Note being rendered
|
||||
* @param array $actions Array containing all Note actions to be rendered
|
||||
*
|
||||
* @return bool Event hook, Event::next (true) is returned to allow Event to be handled by other handlers
|
||||
*/
|
||||
public function onAddNoteActions(Request $request, Note $note, array &$actions): bool
|
||||
public function onAddNoteActions(Request $request, Note $note, array &$actions): EventResult
|
||||
{
|
||||
if (\is_null($user = Common::user())) {
|
||||
return Event::next;
|
||||
@@ -168,10 +167,8 @@ class Favourite extends NoteHandlerPlugin
|
||||
*
|
||||
* @param array $vars Array containing necessary info to process event. In this case, contains the current Note being rendered
|
||||
* @param array $result Contains a hashmap for each Activity performed on Note (object)
|
||||
*
|
||||
* @return bool Event hook, Event::next (true) is returned to allow Event to be handled by other handlers
|
||||
*/
|
||||
public function onAppendCardNote(array $vars, array &$result): bool
|
||||
public function onAppendCardNote(array $vars, array &$result): EventResult
|
||||
{
|
||||
// If note is the original and user isn't the one who repeated, append on end "user repeated this"
|
||||
// If user is the one who repeated, append on end "you repeated this, remove repeat?"
|
||||
@@ -197,7 +194,7 @@ class Favourite extends NoteHandlerPlugin
|
||||
/**
|
||||
* Deletes every favourite entity in table related to a deleted Note
|
||||
*/
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool
|
||||
public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
|
||||
{
|
||||
$note_favourites_list = NoteFavourite::getNoteFavourites($note);
|
||||
foreach ($note_favourites_list as $favourite_entity) {
|
||||
@@ -210,7 +207,7 @@ class Favourite extends NoteHandlerPlugin
|
||||
/**
|
||||
* Maps Routes to their respective Controllers
|
||||
*/
|
||||
public function onAddRoute(Router $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
// Add/remove note to/from favourites
|
||||
$r->connect(id: 'favourite_add', uri_path: '/object/note/{id<\d+>}/favour', target: [Controller\Favourite::class, 'favouriteAddNote']);
|
||||
@@ -233,7 +230,7 @@ class Favourite extends NoteHandlerPlugin
|
||||
*
|
||||
* @throws \App\Util\Exception\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(Feed::create([
|
||||
'actor_id' => $actor_id,
|
||||
@@ -270,10 +267,8 @@ class Favourite extends NoteHandlerPlugin
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
private function activitypub_handler(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): bool
|
||||
private function activitypub_handler(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): EventResult
|
||||
{
|
||||
if (!\in_array($type_activity->get('type'), ['Like', 'Undo'])) {
|
||||
return Event::next;
|
||||
@@ -329,7 +324,7 @@ class Favourite extends NoteHandlerPlugin
|
||||
return Event::stop;
|
||||
}
|
||||
|
||||
public function onActivityPubNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
|
||||
public function onActivityPubNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): EventResult
|
||||
{
|
||||
switch ($activity->getVerb()) {
|
||||
case 'favourite':
|
||||
@@ -363,10 +358,8 @@ class Favourite extends NoteHandlerPlugin
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
public function onNewActivityPubActivity(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, \ActivityPhp\Type\AbstractObject $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): bool
|
||||
public function onNewActivityPubActivity(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, \ActivityPhp\Type\AbstractObject $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): EventResult
|
||||
{
|
||||
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
|
||||
}
|
||||
@@ -387,10 +380,8 @@ class Favourite extends NoteHandlerPlugin
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
public function onNewActivityPubActivityWithObject(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): bool
|
||||
public function onNewActivityPubActivityWithObject(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): EventResult
|
||||
{
|
||||
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
|
||||
}
|
||||
@@ -400,10 +391,8 @@ class Favourite extends NoteHandlerPlugin
|
||||
*
|
||||
* @param string $verb GNU social's internal verb
|
||||
* @param null|string $gs_verb_to_activity_stream_two_verb Resulting Activity Streams 2.0 verb
|
||||
*
|
||||
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
|
||||
*/
|
||||
public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): bool
|
||||
public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): EventResult
|
||||
{
|
||||
if ($verb === 'favourite') {
|
||||
$gs_verb_to_activity_stream_two_verb = 'Like';
|
||||
|
@@ -31,11 +31,13 @@ use App\Core\Modules\Plugin;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use EventResult;
|
||||
|
||||
/**
|
||||
* Check attachment file size quotas
|
||||
*
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @ccategory Attachment
|
||||
*
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
@@ -57,7 +59,7 @@ class FileQuota extends Plugin
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onEnforceUserFileQuota(int $filesize, int $user_id): bool
|
||||
public function onEnforceUserFileQuota(int $filesize, int $user_id): EventResult
|
||||
{
|
||||
$query = <<<'END'
|
||||
select sum(at.size) as total
|
||||
@@ -101,10 +103,8 @@ class FileQuota extends Plugin
|
||||
* Adds this plugin's version information to $versions array
|
||||
*
|
||||
* @param array $versions inherited from parent
|
||||
*
|
||||
* @return bool true hook value
|
||||
*/
|
||||
public function onPluginVersion(array &$versions): bool
|
||||
public function onPluginVersion(array &$versions): EventResult
|
||||
{
|
||||
$versions[] = [
|
||||
'name' => 'FileQuota',
|
||||
|
@@ -32,6 +32,7 @@ use App\Util\Exception\ServerException;
|
||||
use App\Util\Exception\TemporaryFileException;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\TemporaryFile;
|
||||
use EventResult;
|
||||
use Exception;
|
||||
use Jcupitt\Vips;
|
||||
use SplFileInfo;
|
||||
@@ -59,7 +60,7 @@ class ImageEncoder extends Plugin
|
||||
return GSFile::mimetypeMajor($mimetype) === 'image';
|
||||
}
|
||||
|
||||
public function onFileMetaAvailable(array &$event_map, string $mimetype): bool
|
||||
public function onFileMetaAvailable(array &$event_map, string $mimetype): EventResult
|
||||
{
|
||||
if (!self::shouldHandle($mimetype)) {
|
||||
return Event::next;
|
||||
@@ -68,7 +69,7 @@ class ImageEncoder extends Plugin
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onFileSanitizerAvailable(array &$event_map, string $mimetype): bool
|
||||
public function onFileSanitizerAvailable(array &$event_map, string $mimetype): EventResult
|
||||
{
|
||||
if (!self::shouldHandle($mimetype)) {
|
||||
return Event::next;
|
||||
@@ -77,7 +78,7 @@ class ImageEncoder extends Plugin
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onFileResizerAvailable(array &$event_map, string $mimetype): bool
|
||||
public function onFileResizerAvailable(array &$event_map, string $mimetype): EventResult
|
||||
{
|
||||
if (!self::shouldHandle($mimetype)) {
|
||||
return Event::next;
|
||||
@@ -179,7 +180,7 @@ class ImageEncoder extends Plugin
|
||||
/**
|
||||
* Generates the view for attachments of type Image
|
||||
*/
|
||||
public function onViewAttachment(array $vars, array &$res): bool
|
||||
public function onViewAttachment(array $vars, array &$res): EventResult
|
||||
{
|
||||
if (!self::shouldHandle($vars['attachment']->getMimetype())) {
|
||||
return Event::next;
|
||||
@@ -257,10 +258,8 @@ class ImageEncoder extends Plugin
|
||||
* Adds this plugin's version information to $versions array
|
||||
*
|
||||
* @param array $versions inherited from parent
|
||||
*
|
||||
* @return bool true hook value
|
||||
*/
|
||||
public function onPluginVersion(array &$versions): bool
|
||||
public function onPluginVersion(array &$versions): EventResult
|
||||
{
|
||||
$versions[] = [
|
||||
'name' => 'ImageEncoder',
|
||||
|
@@ -33,17 +33,19 @@ namespace Plugin\LatexNotes;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Plugin;
|
||||
use EventResult;
|
||||
use PhpLatex_Parser;
|
||||
use PhpLatex_Renderer_Html;
|
||||
|
||||
class LatexNotes extends Plugin
|
||||
{
|
||||
public function onPostingAvailableContentTypes(array &$types): bool
|
||||
public function onPostingAvailableContentTypes(array &$types): EventResult
|
||||
{
|
||||
$types['LaTeX'] = 'application/x-latex';
|
||||
return Event::next;
|
||||
}
|
||||
public function onRenderNoteContent($content, $content_type, &$rendered): bool
|
||||
|
||||
public function onRenderNoteContent(string $content, string $content_type, string &$rendered): EventResult
|
||||
{
|
||||
if ($content_type !== 'application/x-latex') {
|
||||
return Event::next;
|
||||
|
@@ -33,16 +33,18 @@ namespace Plugin\MarkdownNotes;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Plugin;
|
||||
use EventResult;
|
||||
use Parsedown;
|
||||
|
||||
class MarkdownNotes extends Plugin
|
||||
{
|
||||
public function onPostingAvailableContentTypes(array &$types): bool
|
||||
public function onPostingAvailableContentTypes(array &$types): EventResult
|
||||
{
|
||||
$types['Markdown'] = 'text/markdown';
|
||||
return Event::next;
|
||||
}
|
||||
public function onRenderNoteContent($content, $content_type, &$rendered): bool
|
||||
|
||||
public function onRenderNoteContent(string $content, string $content_type, string &$rendered): EventResult
|
||||
{
|
||||
if ($content_type !== 'text/markdown') {
|
||||
return Event::next;
|
||||
|
@@ -40,6 +40,7 @@ use App\Util\Exception\BugFoundException;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\Functional as GSF;
|
||||
use EventResult;
|
||||
use Functional as F;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -94,7 +95,7 @@ class NoteTypeFeedFilter extends Plugin
|
||||
*
|
||||
* Includes if any positive type matches, but removes if any negated matches
|
||||
*/
|
||||
public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): bool
|
||||
public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): EventResult
|
||||
{
|
||||
$types = $this->normalizeTypesList(\is_null($request->get('note-types')) ? [] : explode(',', $request->get('note-types')));
|
||||
$notes = F\select(
|
||||
@@ -139,7 +140,7 @@ class NoteTypeFeedFilter extends Plugin
|
||||
/**
|
||||
* Draw the media feed navigation.
|
||||
*/
|
||||
public function onAddFeedActions(Request $request, bool $is_not_empty, &$res): bool
|
||||
public function onAddFeedActions(Request $request, bool $is_not_empty, array &$res): EventResult
|
||||
{
|
||||
$qs = [];
|
||||
$query_string = $request->getQueryString();
|
||||
@@ -181,10 +182,8 @@ class NoteTypeFeedFilter extends Plugin
|
||||
* Output our dedicated stylesheet
|
||||
*
|
||||
* @param array $styles stylesheets path
|
||||
*
|
||||
* @return bool hook value; true means continue processing, false means stop
|
||||
*/
|
||||
public function onEndShowStyles(array &$styles, string $route): bool
|
||||
public function onEndShowStyles(array &$styles, string $route): EventResult
|
||||
{
|
||||
$styles[] = 'plugins/NoteTypeFeedFilter/assets/css/noteTypeFeedFilter.css';
|
||||
return Event::next;
|
||||
|
@@ -36,6 +36,9 @@ use League\OAuth2\Server\CryptKey;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use Plugin\OAuth2\Util\Token;
|
||||
|
||||
/**
|
||||
* @extends Token<AccessToken>
|
||||
*/
|
||||
class AccessToken extends Token implements AccessTokenEntityInterface
|
||||
{
|
||||
// {{{ Autocode
|
||||
|
@@ -36,6 +36,9 @@ use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
|
||||
use Plugin\OAuth2\Repository;
|
||||
use Plugin\OAuth2\Util\Token;
|
||||
|
||||
/**
|
||||
* @extends Token<AuthCode>
|
||||
*/
|
||||
class AuthCode extends Token implements AuthCodeEntityInterface
|
||||
{
|
||||
// {{{ Autocode
|
||||
|
@@ -38,6 +38,7 @@ use App\Core\Modules\Plugin;
|
||||
use App\Core\Router;
|
||||
use App\Util\Common;
|
||||
use DateInterval;
|
||||
use EventResult;
|
||||
use Exception;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use League\OAuth2\Server\CryptKey;
|
||||
@@ -67,7 +68,7 @@ class OAuth2 extends Plugin
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onInitializePlugin()
|
||||
public function onInitializePlugin(): EventResult
|
||||
{
|
||||
self::$authorization_server = new AuthorizationServer(
|
||||
new Repository\Client,
|
||||
@@ -86,6 +87,7 @@ class OAuth2 extends Plugin
|
||||
),
|
||||
new DateInterval('PT1H'),
|
||||
);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +96,7 @@ class OAuth2 extends Plugin
|
||||
*
|
||||
* @param Router $r the router that was initialized
|
||||
*/
|
||||
public function onAddRoute(Router $r): bool
|
||||
public function onAddRoute(Router $r): EventResult
|
||||
{
|
||||
$r->connect('oauth2_mastodon_api_apps', '/api/v1/apps', C\Client::class, ['http-methods' => ['POST']]);
|
||||
$r->connect('oauth2_client', '/oauth/client', C\Client::class, ['http-methods' => ['POST']]);
|
||||
@@ -103,7 +105,7 @@ class OAuth2 extends Plugin
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onEndHostMetaLinks(array &$links): bool
|
||||
public function onEndHostMetaLinks(array &$links): EventResult
|
||||
{
|
||||
$links[] = new XML_XRD_Element_Link(self::OAUTH_REQUEST_TOKEN_REL, Router::url('oauth2_client', type: Router::ABSOLUTE_URL));
|
||||
$links[] = new XML_XRD_Element_Link(self::OAUTH_AUTHORIZE_REL, Router::url('oauth2_authorize', type: Router::ABSOLUTE_URL));
|
||||
|
@@ -45,11 +45,17 @@ class RefreshToken implements RefreshTokenRepositoryInterface
|
||||
DB::persist($refreshTokenEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tokenId
|
||||
*/
|
||||
public function revokeRefreshToken($tokenId)
|
||||
{
|
||||
// Some logic to revoke the auth token in a database
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tokenId
|
||||
*/
|
||||
public function isRefreshtokenRevoked($tokenId): bool
|
||||
{
|
||||
return false; // The auth token has not been revoked
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd /var/www/social/file || exit 1
|
||||
mkdir /var/www/social/file && cd /var/www/social/file || exit 1
|
||||
|
||||
mkdir -p oauth && cd oauth || exit 1
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user