From fd71d6ee7d119378bbf504a740a112aefc1996de Mon Sep 17 00:00:00 2001 From: Diogo Peralta Cordeiro Date: Mon, 28 Mar 2022 23:52:48 +0100 Subject: [PATCH] [PLUGIN][UnboundGroup] Finish implementation --- components/Link/Entity/NoteToLink.php | 2 +- plugins/ActivityPub/ActivityPub.php | 20 ++++--- plugins/ActivityPub/Util/Model/Actor.php | 1 + .../UnboundGroup/Controller/GroupSettings.php | 59 ++++++++++++++++++- .../Entity/activitypubGroupUnbound.php | 53 +++++++++++++++++ plugins/UnboundGroup/UnboundGroup.php | 46 +++++++++++++++ plugins/WebMonetization/WebMonetization.php | 13 +++- 7 files changed, 182 insertions(+), 12 deletions(-) create mode 100644 plugins/UnboundGroup/Entity/activitypubGroupUnbound.php diff --git a/components/Link/Entity/NoteToLink.php b/components/Link/Entity/NoteToLink.php index 5e3274aad7..17fe7a8281 100644 --- a/components/Link/Entity/NoteToLink.php +++ b/components/Link/Entity/NoteToLink.php @@ -91,7 +91,7 @@ class NoteToLink extends Entity $note = DB::find('note', ['id' => $args['note_id']]); Event::handle('NewLinkFromNote', [$link, $note]); $obj = new self(); - return parent::createOrUpdate($args, $obj); + return parent::createOrUpdate(obj: $obj, args: $args); } public static function removeWhereNoteId(int $note_id): mixed diff --git a/plugins/ActivityPub/ActivityPub.php b/plugins/ActivityPub/ActivityPub.php index 84df4e4e23..17692c763c 100644 --- a/plugins/ActivityPub/ActivityPub.php +++ b/plugins/ActivityPub/ActivityPub.php @@ -324,13 +324,19 @@ class ActivityPub extends Plugin ): bool { try { $data = Model::toType($activity); - if ($sender->isGroup() && ($activity->getVerb() !== 'subscribe' || !($activity->getVerb() === 'undo' && $data->get('object')->get('type') === 'Follow'))) { - // When the sender is a group, we have to wrap it in a transient Announce activity - $data = Type::create('Announce', [ - '@context' => 'https:\/\/www.w3.org\/ns\/activitystreams', - 'actor' => $sender->getUri(type: Router::ABSOLUTE_URL), - 'object' => $data, - ]); + if ($sender->isGroup()) { // When the sender is a group, + if ($activity->getVerb() === 'subscribe') { + // Regular postman happens + } elseif ($activity->getVerb() === 'undo' && $data->get('object')->get('type') === 'Follow') { + // Regular postman happens + } else { + // For every other activity sent by a Group, we have to wrap it in a transient Announce activity + $data = Type::create('Announce', [ + '@context' => 'https:\/\/www.w3.org\/ns\/activitystreams', + 'actor' => $sender->getUri(type: Router::ABSOLUTE_URL), + 'object' => $data, + ]); + } } $res = self::postman($sender, $data->toJson(), $inbox); diff --git a/plugins/ActivityPub/Util/Model/Actor.php b/plugins/ActivityPub/Util/Model/Actor.php index 62e2d5c377..0d239f3c8a 100644 --- a/plugins/ActivityPub/Util/Model/Actor.php +++ b/plugins/ActivityPub/Util/Model/Actor.php @@ -198,6 +198,7 @@ class Actor extends Model } } + Event::handle('ActivityPubCreateOrUpdateActor', [$object, &$actor, &$ap_actor]); return $ap_actor; } diff --git a/plugins/UnboundGroup/Controller/GroupSettings.php b/plugins/UnboundGroup/Controller/GroupSettings.php index 7672a0537b..74f10d9f49 100644 --- a/plugins/UnboundGroup/Controller/GroupSettings.php +++ b/plugins/UnboundGroup/Controller/GroupSettings.php @@ -5,6 +5,7 @@ declare(strict_types = 1); namespace Plugin\UnboundGroup\Controller; use App\Core\Controller; +use App\Core\DB; use App\Core\Form; use function App\Core\I18n\_m; use App\Entity as E; @@ -12,12 +13,60 @@ use App\Util\Common; use App\Util\Exception\ClientException; use Component\Subscription\Subscription; use Plugin\ActivityPub\Util\Explorer; +use Plugin\UnboundGroup\Entity\activitypubGroupUnbound; +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; class GroupSettings extends Controller { + /** + * Manage the gs:unbound state + */ + public static function groupUnbound(Request $request, E\Actor $target, string $details_id) + { + $actor = Common::ensureLoggedIn()->getActor(); + if (!$actor->canModerate($target)) { + throw new ClientException(_m('You don\'t have enough permissions to edit {nickname}\'s settings', ['{nickname}' => $target->getNickname()])); + } + + $unbound_options = [ + _m('Default') => null, + _m('True') => true, + _m('False') => false, + ]; + + $obj = DB::findOneBy(activitypubGroupUnbound::class, ['actor_id' => $target->getId()], return_null: true); + + $form_definition = [ + ['new_state', ChoiceType::class, ['label' => _m('Unbound:'), 'multiple' => false, 'expanded' => false, 'choices' => $unbound_options, 'data' => $obj?->getUnbound()]], + [$save_unbound_state_form_name = 'save_group_links', SubmitType::class, ['label' => _m('Save state')]], + ]; + + $unbound_state_form = Form::create($form_definition); + + if ($request->getMethod() === 'POST' && $request->request->has($save_unbound_state_form_name)) { + $unbound_state_form->handleRequest($request); + if ($unbound_state_form->isSubmitted() && $unbound_state_form->isValid()) { + $new_state = $unbound_state_form->getData()['new_state']; + $update_obj = activitypubGroupUnbound::createOrUpdate(obj: $obj, args: [ + 'actor_id' => $target->getId(), + 'unbound' => $new_state, + ]); + if (\is_null($obj)) { + DB::persist($update_obj); + } + DB::flush(); + Form::forceRedirect($unbound_state_form, $request); + } + } + + return [ + '_template' => 'self_tags_settings.fragment.html.twig', + 'add_self_tags_form' => $unbound_state_form->createView(), + ]; + } /** * Manage the linksTo collection of a Group or Organisation Actor */ @@ -40,10 +89,14 @@ class GroupSettings extends Controller if ($add_link_to_form->isSubmitted() && $add_link_to_form->isValid()) { if (Common::isValidHttpUrl($new_uri = $add_link_to_form->getData()['new_link'])) { $new_link = Explorer::getOneFromUri($new_uri); - if (\is_null(Subscription::subscribe(subject: $target, object: $new_link, source: 'ActivityPub via FEP-2100'))) { - throw new ClientException(_m('This group is already linked from {nickname}', ['{nickname}' => $new_link->getNickname()])); + + $unbound = DB::findOneBy(activitypubGroupUnbound::class, ['actor_id' => $new_link->getId()], return_null: true); + if ($unbound?->getUnbound() ?? true) { + if (\is_null(Subscription::subscribe(subject: $target, object: $new_link, source: 'ActivityPub via FEP-2100'))) { + throw new ClientException(_m('This group is already linked from {nickname}', ['{nickname}' => $new_link->getNickname()])); + } + Subscription::refreshSubscriptionCount($target, $new_link); } - Subscription::refreshSubscriptionCount($target, $new_link); Form::forceRedirect($add_link_to_form, $request); } else { throw new ClientException(_m('Invalid URI given.')); diff --git a/plugins/UnboundGroup/Entity/activitypubGroupUnbound.php b/plugins/UnboundGroup/Entity/activitypubGroupUnbound.php new file mode 100644 index 0000000000..66c9dd4b82 --- /dev/null +++ b/plugins/UnboundGroup/Entity/activitypubGroupUnbound.php @@ -0,0 +1,53 @@ +actor_id = $actor_id; + return $this; + } + + public function getActorId(): int + { + return $this->actor_id; + } + + public function setUnbound(?bool $unbound): self + { + $this->unbound = $unbound; + return $this; + } + + public function getUnbound(): ?bool + { + return $this->unbound; + } + + // @codeCoverageIgnoreEnd + // }}} Autocode + + public static function schemaDef(): array + { + return [ + 'name' => 'activitypubGroupUnbound', + 'fields' => [ + 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'foreign key to actor table'], + 'unbound' => ['type' => 'bool', 'not null' => false, 'description' => 'gs:unbound'], + ], + 'primary key' => ['actor_id'], + ]; + } +} diff --git a/plugins/UnboundGroup/UnboundGroup.php b/plugins/UnboundGroup/UnboundGroup.php index 15c676f414..a8653f1ef7 100644 --- a/plugins/UnboundGroup/UnboundGroup.php +++ b/plugins/UnboundGroup/UnboundGroup.php @@ -32,10 +32,13 @@ declare(strict_types = 1); namespace Plugin\UnboundGroup; +use App\Core\DB; use App\Core\Event; use App\Core\Modules\Plugin; use App\Entity\Actor; +use Plugin\ActivityPub\Entity\ActivitypubActor; use Plugin\UnboundGroup\Controller\GroupSettings; +use Plugin\UnboundGroup\Entity\activitypubGroupUnbound; use Symfony\Component\HttpFoundation\Request; /** @@ -49,6 +52,12 @@ class UnboundGroup extends Plugin public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool { if ($section === 'profile' && $request->get('_route') === 'group_actor_settings') { + $tabs[] = [ + 'title' => 'Unbound', + 'desc' => 'Unbound Group state.', + 'id' => 'settings-group-unbound-details', + 'controller' => GroupSettings::groupUnbound($request, Actor::getById((int) $request->get('id')), 'settings-group-unbound-details'), + ]; $tabs[] = [ 'title' => 'Linked', 'desc' => 'Link to this Group from another Group.', @@ -58,4 +67,41 @@ class UnboundGroup extends Plugin } return Event::next; } + + public function onActivityStreamsTwoContext(array &$activity_streams_two_context): bool + { + $activity_streams_two_context[] = [ + 'unbound' => [ + '@id' => 'gs:unbound', + '@type' => '@id', + ], + ]; + return Event::next; + } + + public function onActivityPubAddActivityStreamsTwoData(string $type_name, &$type): bool + { + if ($type_name === 'Group' || $type_name === 'Organization') { + $actor = \Plugin\ActivityPub\Util\Explorer::getOneFromUri($type->getId()); + $unbound = DB::findOneBy(activitypubGroupUnbound::class, ['actor_id' => $actor->getId()], return_null: true); + + if (!\is_null($unbound_value = $unbound?->getUnbound())) { + $type->set('unbound', $unbound_value); + } + } + return Event::next; + } + + public function onActivityPubCreateOrUpdateActor(\ActivityPhp\Type\AbstractObject $object, Actor $actor, ActivitypubActor $ap_actor): bool + { + if ($object->has('unbound')) { + $obj = DB::findOneBy(activitypubGroupUnbound::class, ['actor_id' => $actor->getId()], return_null: true); + $update_obj = activitypubGroupUnbound::createOrUpdate(obj: $obj, args: ['actor_id' => $actor->getId(), 'unbound' => $object->get('unbound')]); + if (\is_null($obj)) { + DB::persist($update_obj); + } + DB::flush(); + } + return Event::next; + } } diff --git a/plugins/WebMonetization/WebMonetization.php b/plugins/WebMonetization/WebMonetization.php index 48a92fc4b2..a7a6d2b5ba 100644 --- a/plugins/WebMonetization/WebMonetization.php +++ b/plugins/WebMonetization/WebMonetization.php @@ -43,6 +43,7 @@ use App\Entity\LocalUser; use App\Util\Common; use App\Util\Exception\RedirectException; use App\Util\Formatting; +use Plugin\ActivityPub\Entity\ActivitypubActor; use Plugin\WebMonetization\Entity\Wallet; use Plugin\WebMonetization\Entity\WebMonetization as Monetization; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -66,7 +67,7 @@ class WebMonetization extends Plugin if ($vars['path'] === 'settings') { $is_self = true; - } elseif ($vars['path'] === 'actor_view_nickname') { + } elseif ($vars['path'] === 'actor_view_nickname') { $is_self = $request->attributes->get('nickname') === $user->getNickname(); if (!$is_self) { $receiver_id = DB::findOneBy(LocalUser::class, [ @@ -263,4 +264,14 @@ class WebMonetization extends Plugin } return Event::next; } + + public function onActivityPubCreateOrUpdateActor(\ActivityPhp\Type\AbstractObject $object, Actor $actor, ActivitypubActor $ap_actor): bool + { + if ($object->has('webmonetizationWallet')) { + $attr = ['actor_id' => $actor->getId(), 'address' => $object->get('webmonetizationWallet')]; + $obj = DB::findOneBy(Wallet::class, $attr, return_null: true); + DB::persist(Wallet::createOrUpdate(obj: $obj, args: $attr)); + } + return Event::next; + } }