[CORE][SCOPE] Implement basic visibility in feeds
This commit is contained in:
parent
d4bc1d097d
commit
3e13765f62
@ -67,7 +67,7 @@ class Feed extends Component
|
||||
}
|
||||
$actors = $actor_qb->getQuery()->execute();
|
||||
|
||||
// TODO: Enforce scoping on the notes before returning
|
||||
// N.B.: Scope is only enforced at FeedController level
|
||||
return ['notes' => $notes ?? null, 'actors' => $actors ?? null];
|
||||
}
|
||||
|
||||
|
@ -34,25 +34,66 @@ namespace Component\Feed\Util;
|
||||
|
||||
use App\Core\Controller;
|
||||
use App\Core\Event;
|
||||
use App\Core\Log;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use function array_key_exists;
|
||||
|
||||
abstract class FeedController extends Controller
|
||||
{
|
||||
/**
|
||||
* Post process the result of a feed controller, to remove any
|
||||
* Post-processing of the result of a feed controller, to remove any
|
||||
* notes or actors the user specified, as well as format the raw
|
||||
* list of notes into a usable format
|
||||
*/
|
||||
public static function post_process(array $result): array
|
||||
public static function postProcess(array $result): array
|
||||
{
|
||||
$actor = Common::actor();
|
||||
|
||||
if (\array_key_exists('notes', $result)) {
|
||||
if (array_key_exists('notes', $result)) {
|
||||
$notes = $result['notes'];
|
||||
self::enforceScope($notes, $actor);
|
||||
Event::handle('FilterNoteList', [$actor, &$notes, $result['request']]);
|
||||
Event::handle('FormatNoteList', [$notes, &$result['notes']]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function enforceScope(array &$notes, ?Actor $actor): void
|
||||
{
|
||||
$filtered_notes = [];
|
||||
foreach($notes as $note) {
|
||||
switch($note->getScope()) {
|
||||
case VisibilityScope::LOCAL: // The controller handles it if private
|
||||
case VisibilityScope::PUBLIC:
|
||||
$filtered_notes[] = $note;
|
||||
break;
|
||||
case VisibilityScope::ADDRESSEE:
|
||||
// If the actor is logged in and
|
||||
if (!is_null($actor) &&
|
||||
(
|
||||
// Is either the author Or
|
||||
$note->getActorId() == $actor->getId() ||
|
||||
// one of the targets
|
||||
in_array($actor->getId(), $note->getNotificationTargetIds())
|
||||
)) {
|
||||
$filtered_notes[] = $note;
|
||||
}
|
||||
break;
|
||||
case VisibilityScope::GROUP:
|
||||
// Only for the group to see
|
||||
break;
|
||||
case VisibilityScope::COLLECTION: // no break
|
||||
case VisibilityScope::MESSAGE:
|
||||
// Only for the collection to see (they will only find it in their notifications)
|
||||
break;
|
||||
default:
|
||||
Log::warning("Unknown scope found: {$note->getScope()}.");
|
||||
}
|
||||
}
|
||||
// Replace notes with filtered ones I/O
|
||||
$notes = $filtered_notes;
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Util\Exception\BugFoundException;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\Router;
|
||||
@ -56,6 +58,8 @@ use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use function count;
|
||||
use function is_null;
|
||||
|
||||
class Posting extends Component
|
||||
{
|
||||
@ -69,7 +73,7 @@ class Posting extends Component
|
||||
*/
|
||||
public function onAppendRightPostingBlock(Request $request, array &$res): bool
|
||||
{
|
||||
if (\is_null($user = Common::user())) {
|
||||
if (is_null($user = Common::user())) {
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
@ -99,12 +103,17 @@ class Posting extends Component
|
||||
$form_params[] = ['in', ChoiceType::class, ['label' => _m('In:'), 'multiple' => false, 'expanded' => false, 'choices' => $in_targets]];
|
||||
}
|
||||
|
||||
$form_params[] = ['visibility', ChoiceType::class, ['label' => _m('Visibility:'), 'multiple' => false, 'expanded' => false, 'data' => 'public', 'choices' => [_m('Public') => 'public', _m('Instance') => 'instance', _m('Private') => 'private']]];
|
||||
// TODO: if in group page, add GROUP visibility to the choices.
|
||||
$form_params[] = ['visibility', ChoiceType::class, ['label' => _m('Visibility:'), 'multiple' => false, 'expanded' => false, 'data' => 'public', 'choices' => [
|
||||
_m('Public') => VisibilityScope::PUBLIC,
|
||||
_m('Local') => VisibilityScope::LOCAL,
|
||||
_m('Addressee') => VisibilityScope::ADDRESSEE
|
||||
]]];
|
||||
$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.')]];
|
||||
$form_params[] = FormFields::language($actor, $context_actor, label: _m('Note language'), help: _m('The selected language will be federated and added as a lang attribute, preferred language can be set up in settings'));
|
||||
|
||||
if (\count($available_content_types) > 1) {
|
||||
if (count($available_content_types) > 1) {
|
||||
$form_params[] = ['content_type', ChoiceType::class,
|
||||
[
|
||||
'label' => _m('Text format:'), 'multiple' => false, 'expanded' => false,
|
||||
@ -126,7 +135,11 @@ class Posting extends Component
|
||||
$data = $form->getData();
|
||||
if (empty($data['content']) && empty($data['attachments'])) {
|
||||
// TODO Display error: At least one of `content` and `attachments` must be provided
|
||||
throw new ClientException(_m('You must enter content or provide at least one attachment to post a note'));
|
||||
throw new ClientException(_m('You must enter content or provide at least one attachment to post a note.'));
|
||||
}
|
||||
|
||||
if (!VisibilityScope::isValue($data['visibility'])) {
|
||||
throw new ClientException(_m('You have selected an impossible visibility.'));
|
||||
}
|
||||
|
||||
$content_type = $data['content_type'] ?? $available_content_types[array_key_first($available_content_types)];
|
||||
@ -134,12 +147,13 @@ class Posting extends Component
|
||||
Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form_params, $form]);
|
||||
|
||||
self::storeLocalNote(
|
||||
$user->getActor(),
|
||||
$data['content'],
|
||||
$content_type,
|
||||
$data['language'],
|
||||
$data['attachments'],
|
||||
actor: $user->getActor(),
|
||||
content: $data['content'],
|
||||
content_type: $content_type,
|
||||
language: $data['language'],
|
||||
scope: $data['visibility'],
|
||||
target: $data['in'] ?? null,
|
||||
attachments: $data['attachments'],
|
||||
process_note_content_extra_args: $extra_args,
|
||||
);
|
||||
|
||||
@ -160,26 +174,34 @@ class Posting extends Component
|
||||
* $actor_id, possibly as a reply to note $reply_to and with flag
|
||||
* $is_local. Sanitizes $content and $attachments
|
||||
*
|
||||
* @param array $attachments Array of 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 Actor $actor
|
||||
* @param string|null $content
|
||||
* @param string $content_type
|
||||
* @param string|null $language
|
||||
* @param int|null $scope
|
||||
* @param string|null $target
|
||||
* @param array $attachments Array of 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
|
||||
*
|
||||
* @return Note
|
||||
* @throws ClientException
|
||||
* @throws DuplicateFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return Entity|mixed
|
||||
* @throws BugFoundException
|
||||
*/
|
||||
public static function storeLocalNote(
|
||||
Actor $actor,
|
||||
Actor $actor,
|
||||
?string $content,
|
||||
string $content_type,
|
||||
string $content_type,
|
||||
?string $language = null,
|
||||
array $attachments = [],
|
||||
array $processed_attachments = [],
|
||||
?int $scope = null,
|
||||
?string $target = null,
|
||||
array $process_note_content_extra_args = [],
|
||||
) {
|
||||
array $attachments = [],
|
||||
array $processed_attachments = [],
|
||||
array $process_note_content_extra_args = [],
|
||||
): Note {
|
||||
$scope ??= VisibilityScope::PUBLIC; // TODO: If site is private, default to LOCAL
|
||||
$rendered = null;
|
||||
$mentions = [];
|
||||
if (!empty($content)) {
|
||||
@ -191,8 +213,9 @@ class Posting extends Component
|
||||
'content' => $content,
|
||||
'content_type' => $content_type,
|
||||
'rendered' => $rendered,
|
||||
'language_id' => !\is_null($language) ? Language::getByLocale($language)->getId() : null,
|
||||
'language_id' => !is_null($language) ? Language::getByLocale($language)->getId() : null,
|
||||
'is_local' => true,
|
||||
'scope' => $scope,
|
||||
]);
|
||||
|
||||
/** @var UploadedFile[] $attachments */
|
||||
@ -238,7 +261,7 @@ class Posting extends Component
|
||||
]);
|
||||
DB::persist($activity);
|
||||
|
||||
if (!\is_null($target)) {
|
||||
if (!is_null($target)) {
|
||||
switch ($target[0]) {
|
||||
case '!':
|
||||
$mentions[] = [
|
||||
@ -254,7 +277,7 @@ class Posting extends Component
|
||||
|
||||
$mentioned = [];
|
||||
foreach (F\unique(F\flat_map($mentions, fn (array $m) => $m['mentioned'] ?? []), fn (Actor $a) => $a->getId()) as $m) {
|
||||
if (!\is_null($m)) {
|
||||
if (!is_null($m)) {
|
||||
$mentioned[] = $m->getId();
|
||||
|
||||
if ($m->isGroup()) {
|
||||
|
@ -44,12 +44,6 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Feeds extends FeedController
|
||||
{
|
||||
// Can't have constants inside herestring
|
||||
private $public_scope = VisibilityScope::PUBLIC;
|
||||
private $instance_scope = VisibilityScope::PUBLIC | VisibilityScope::SITE;
|
||||
private $message_scope = VisibilityScope::MESSAGE;
|
||||
private $subscriber_scope = VisibilityScope::PUBLIC | VisibilityScope::SUBSCRIBER;
|
||||
|
||||
/**
|
||||
* The Planet feed represents every local post. Which is what this instance has to share with the universe.
|
||||
*/
|
||||
@ -63,7 +57,6 @@ class Feeds extends FeedController
|
||||
return [
|
||||
'_template' => 'feed/feed.html.twig',
|
||||
'page_title' => _m(\is_null(Common::user()) ? 'Feed' : 'Planet'),
|
||||
'should_format' => true,
|
||||
'notes' => $data['notes'],
|
||||
];
|
||||
}
|
||||
@ -83,7 +76,6 @@ class Feeds extends FeedController
|
||||
return [
|
||||
'_template' => 'feed/feed.html.twig',
|
||||
'page_title' => _m('Home'),
|
||||
'should_format' => true,
|
||||
'notes' => $data['notes'],
|
||||
];
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ abstract class Controller extends AbstractController implements EventSubscriberI
|
||||
|
||||
// XXX: Could we do this differently?
|
||||
if (is_subclass_of($controller, FeedController::class)) {
|
||||
$this->vars = FeedController::post_process($this->vars);
|
||||
$this->vars = FeedController::postProcess($this->vars);
|
||||
}
|
||||
|
||||
// Respond in the most preferred acceptable content type
|
||||
|
@ -25,12 +25,10 @@ use App\Util\Bitmap;
|
||||
|
||||
class VisibilityScope extends Bitmap
|
||||
{
|
||||
public const PUBLIC = 1;
|
||||
public const SITE = 2;
|
||||
public const ADDRESSEE = 4;
|
||||
public const GROUP = 8;
|
||||
public const SUBSCRIBER = 16;
|
||||
public const MESSAGE = 32;
|
||||
|
||||
public static int $instance_scope = self::PUBLIC | self::SITE;
|
||||
public const PUBLIC = 1; // Can be shown everywhere (default)
|
||||
public const LOCAL = 2; // Non-public and non-federated (default in private sites)
|
||||
public const ADDRESSEE = 4; // Only if the actor is the author or one of the targets
|
||||
public const GROUP = 8; // Only in the Group feed
|
||||
public const COLLECTION = 16; // Only for the collection to see (same as addressee but not available in feeds, notifications only)
|
||||
public const MESSAGE = 32; // Direct Message (same as Collection, but also with dedicated plugin)
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ class Note extends Entity
|
||||
private ?int $reply_to;
|
||||
private bool $is_local;
|
||||
private ?string $source;
|
||||
private int $scope = 1;
|
||||
private int $scope = VisibilityScope::PUBLIC;
|
||||
private ?string $url;
|
||||
private ?int $language_id;
|
||||
private DateTimeInterface $created;
|
||||
@ -468,7 +468,7 @@ class Note extends Entity
|
||||
'reply_to' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'note replied to, null if root of a conversation'],
|
||||
'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'was this note generated by a local actor'],
|
||||
'source' => ['type' => 'varchar', 'foreign key' => true, 'length' => 32, 'target' => 'NoteSource.code', 'multiplicity' => 'many to one', 'description' => 'fkey to source of note, like "web", "im", or "clientname"'],
|
||||
'scope' => ['type' => 'int', 'not null' => true, 'default' => VisibilityScope::PUBLIC, 'description' => 'bit map for distribution scope; 0 = everywhere; 1 = this server only; 2 = addressees; 4 = groups; 8 = subscribers; 16 = messages; null = default'],
|
||||
'scope' => ['type' => 'int', 'not null' => true, 'default' => VisibilityScope::PUBLIC, 'description' => 'bit map for distribution scope; 1 = everywhere; 2 = this server only; 4 = addressees; 8 = groups; 16 = collection; 32 = messages'],
|
||||
'url' => ['type' => 'text', 'description' => 'Permalink to Note'],
|
||||
'language_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Language.id', 'multiplicity' => 'one to many', 'description' => 'The language for this note'],
|
||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||
|
@ -86,4 +86,9 @@ abstract class Bitmap
|
||||
{
|
||||
return self::_to($r, false);
|
||||
}
|
||||
|
||||
public static function isValue(int $value): bool
|
||||
{
|
||||
return in_array($value, (new ReflectionClass(static::class))->getConstants());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user