diff --git a/components/Group/Controller/Group.php b/components/Group/Controller/Group.php index 3a88f6b99b..70434ee3af 100644 --- a/components/Group/Controller/Group.php +++ b/components/Group/Controller/Group.php @@ -208,6 +208,10 @@ class Group extends FeedController _m('Group') => 'group', _m('Organisation') => 'organisation', ]]], + ['group_scope', ChoiceType::class, ['label' => _m('Is this a private group:'), 'multiple' => false, 'expanded' => false, 'choices' => [ + _m('No') => 'public', + _m('Yes') => 'private', + ]]], ['group_create', SubmitType::class, ['label' => _m('Create this group!')]], ]); @@ -221,9 +225,15 @@ class Group extends FeedController check_is_allowed: true ); + $roles = ActorLocalRoles::VISITOR; // Can send direct messages to other actors + + if ($data['group_scope'] === 'private') { + $roles |= ActorLocalRoles::PRIVATE_GROUP; + } + Log::info( _m( - 'Actor id:{actor_id} nick:{actor_nick} created the group {nickname}', + 'Actor id:{actor_id} nick:{actor_nick} created the '.($roles & ActorLocalRoles::PRIVATE_GROUP ? 'private' : 'public').' group {nickname}', ['{actor_id}' => $actor->getId(), 'actor_nick' => $actor->getNickname(), 'nickname' => $nickname], ), ); @@ -232,7 +242,7 @@ class Group extends FeedController 'nickname' => $nickname, 'type' => E\Actor::GROUP, 'is_local' => true, - 'roles' => ActorLocalRoles::VISITOR, // Can send direct messages to other actors + 'roles' => $roles, ])); DB::persist(LocalGroup::create([ 'actor_id' => $group->getId(), diff --git a/components/Posting/Posting.php b/components/Posting/Posting.php index 65827bef64..9138c8112a 100644 --- a/components/Posting/Posting.php +++ b/components/Posting/Posting.php @@ -23,6 +23,7 @@ declare(strict_types = 1); namespace Component\Posting; +use App\Core\ActorLocalRoles; use App\Core\DB\DB; use App\Core\Event; use App\Core\Form; @@ -119,9 +120,15 @@ class Posting extends Component _m('Addressee') => VisibilityScope::ADDRESSEE->value, ]; if (!is_null($context_actor) && $context_actor->isGroup()) { - $visibility_options[_m('Group')] = VisibilityScope::GROUP->value; + if ($actor->canAdmin($context_actor)) { + if ($context_actor->getRoles() & ActorLocalRoles::PRIVATE_GROUP) { + $visibility_options = array_merge([_m('Group') => VisibilityScope::GROUP->value], $visibility_options); + } else { + $visibility_options[_m('Group')] = VisibilityScope::GROUP->value; + } + } } - $form_params[] = ['visibility', ChoiceType::class, ['label' => _m('Visibility:'), 'multiple' => false, 'expanded' => false, 'data' => 'public', 'choices' => $visibility_options]]; + $form_params[] = ['visibility', ChoiceType::class, ['label' => _m('Visibility:'), 'multiple' => false, 'expanded' => false, 'choices' => $visibility_options]]; $form_params[] = ['content', TextareaType::class, ['label' => _m('Content:'), 'data' => $initial_content, 'attr' => ['placeholder' => _m($placeholder)], 'constraints' => [new Length(['max' => Common::config('site', 'text_limit')])]]]; $form_params[] = ['attachments', FileType::class, ['label' => _m('Attachments:'), 'multiple' => true, 'required' => false, 'invalid_message' => _m('Attachment not valid.')]]; diff --git a/plugins/ActivityPub/Util/Model/Actor.php b/plugins/ActivityPub/Util/Model/Actor.php index 5cf2032da1..42082bc90c 100644 --- a/plugins/ActivityPub/Util/Model/Actor.php +++ b/plugins/ActivityPub/Util/Model/Actor.php @@ -83,18 +83,35 @@ class Actor extends Model */ public static function fromJson(string|AbstractObject $json, array $options = []): ActivitypubActor { - $person = \is_string($json) ? self::jsonToType($json) : $json; + $object = \is_string($json) ? self::jsonToType($json) : $json; + + switch ($object->get('type')) { + case 'Application': + case 'Person': + // TODO: Operator may prefer users to start with Visitor and then have them being manually promoted + $roles = ActorLocalRoles::PARTICIPANT | ActorLocalRoles::VISITOR; // Can view and participate + break; + case 'Group': + case 'Organisation': + $roles = ActorLocalRoles::VISITOR; // Can send direct messages to other actors + if ($object->get('private')) { + $roles |= ActorLocalRoles::PRIVATE_GROUP; + } + break; + case 'Service': + default: + $roles = ActorLocalRoles::NONE; + } // Actor $actor_map = [ - 'nickname' => $person->get('preferredUsername'), - 'fullname' => !empty($person->get('name')) ? $person->get('name') : null, - 'created' => new DateTime($person->get('published') ?? 'now'), - 'bio' => $person->get('summary'), + 'nickname' => $object->get('preferredUsername'), + 'fullname' => !empty($object->get('name')) ? $object->get('name') : null, + 'created' => new DateTime($object->get('published') ?? 'now'), + 'bio' => $object->get('summary'), 'is_local' => false, // duh! - 'type' => self::$_as2_actor_type_to_gs_actor_type[$person->get('type')], - // TODO: Operator may prefer users to start with Visitor and then have them being manually promoted - 'roles' => ActorLocalRoles::PARTICIPANT | ActorLocalRoles::VISITOR, // Can view and participate + 'type' => self::$_as2_actor_type_to_gs_actor_type[$object->get('type')], + 'roles' => $roles, 'modified' => new DateTime(), ]; @@ -111,11 +128,11 @@ class Actor extends Model // ActivityPub Actor $ap_actor = ActivitypubActor::create([ - 'inbox_uri' => $person->get('inbox'), - 'inbox_shared_uri' => ($person->has('endpoints') && isset($person->get('endpoints')['sharedInbox'])) ? $person->get('endpoints')['sharedInbox'] : null, - 'uri' => $person->get('id'), + 'inbox_uri' => $object->get('inbox'), + 'inbox_shared_uri' => ($object->has('endpoints') && isset($object->get('endpoints')['sharedInbox'])) ? $object->get('endpoints')['sharedInbox'] : null, + 'uri' => $object->get('id'), 'actor_id' => $actor->getId(), - 'url' => $person->get('url') ?? null, + 'url' => $object->get('url') ?? null, ], $options['objects']['ActivitypubActor'] ?? null); if (!isset($options['objects']['ActivitypubActor'])) { @@ -125,7 +142,7 @@ class Actor extends Model // Public Key $apRSA = ActivitypubRsa::create([ 'actor_id' => $actor->getID(), - 'public_key' => ($person->has('publicKey') && isset($person->get('publicKey')['publicKeyPem'])) ? $person->get('publicKey')['publicKeyPem'] : null, + 'public_key' => ($object->has('publicKey') && isset($object->get('publicKey')['publicKeyPem'])) ? $object->get('publicKey')['publicKeyPem'] : null, ], $options['objects']['ActivitypubRsa'] ?? null); if (!isset($options['objects']['ActivitypubRsa'])) { @@ -133,10 +150,10 @@ class Actor extends Model } // Avatar - if ($person->has('icon') && !empty($person->get('icon'))) { + if ($object->has('icon') && !empty($object->get('icon'))) { try { // Retrieve media - $get_response = HTTPClient::get($person->get('icon')->get('url')); + $get_response = HTTPClient::get($object->get('icon')->get('url')); $media = $get_response->getContent(); $mimetype = $get_response->getHeaders()['content-type'][0] ?? null; unset($get_response); @@ -153,12 +170,12 @@ class Actor extends Model if (!\is_null($avatar = DB::findOneBy(\Component\Avatar\Entity\Avatar::class, ['actor_id' => $actor->getId()], return_null: true))) { $avatar->delete(); } - DB::wrapInTransaction(function () use ($attachment, $actor, $person) { + DB::wrapInTransaction(function () use ($attachment, $actor, $object) { DB::persist($attachment); DB::persist(\Component\Avatar\Entity\Avatar::create([ 'actor_id' => $actor->getId(), 'attachment_id' => $attachment->getId(), - 'title' => $person->get('icon')->get('name') ?? null, + 'title' => $object->get('icon')->get('name') ?? null, ])); }); Event::handle('AvatarUpdate', [$actor->getId()]); @@ -255,6 +272,12 @@ class Actor extends Model // No icon for this actor } + if ($object->isGroup()) { + if ($object->getRoles() & ActorLocalRoles::PRIVATE_GROUP) { + $attr['private'] = true; + } + } + $type = self::jsonToType($attr); Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); return $type->toJson($options);