forked from GNUsocial/gnu-social
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
b999c1bd62
|
|||
9dc6243822
|
|||
ce8f54dc46
|
|||
9e7db08e50
|
|||
841d10cde0
|
|||
95c8f3bdc7
|
|||
b82818646f
|
|||
5ac764f3e5
|
|||
4ad1de2616
|
|||
29f53bb698
|
|||
cb16b627b4
|
|||
19dd4ba368
|
|||
53a1a3fad1
|
|||
737648359d
|
|||
57c09c6f8f
|
|||
08e3da092b
|
|||
7959ea497b
|
52
Makefile
52
Makefile
@@ -69,54 +69,4 @@ remove-file:
|
||||
flush-redis-cache:
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_redis_1) sh -c 'redis-cli flushall'
|
||||
|
||||
install-plugins:
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_php_1) /var/www/social/bin/install_plugins.sh
|
||||
|
||||
update-dependencies:
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_php_1) sh -c 'cd /var/www/social && composer update'
|
||||
|
||||
update-autocode:
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_php_1) sh -c 'cd /var/www/social && bin/update_autocode'
|
||||
|
||||
backup-actors:
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_db_1) \
|
||||
sh -c 'su postgres -c "mkdir -p /tmp/backup"' && \
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_php_1) \
|
||||
sh -c "cd /var/www/social && bin/console doctrine:query:sql \"\
|
||||
copy actor to '/tmp/backup/actor.csv';\
|
||||
copy local_user to '/tmp/backup/local_user.csv';\
|
||||
copy local_group to '/tmp/backup/local_group.csv';\
|
||||
\
|
||||
copy activitypub_actor to '/tmp/backup/ap_actor.csv';\
|
||||
copy activitypub_rsa to '/tmp/backup/ap_rsa.csv';\
|
||||
\
|
||||
copy actor_subscription to '/tmp/backup/actor_subscription.csv';\
|
||||
copy group_member to '/tmp/backup/group_member.csv';\
|
||||
\
|
||||
copy feed to '/tmp/backup/feed.csv';\
|
||||
copy (SELECT 'ALTER SEQUENCE ' || c.relname || ' RESTART WITH ' || nextval(c.relname::regclass) || ';'\
|
||||
FROM pg_class c WHERE c.relkind = 'S') to '/tmp/backup/sequences';\"" && \
|
||||
mkdir -p /tmp/social-sql-backup && \
|
||||
docker cp $(call translate-container-name,$(strip $(DIR))_db_1):/tmp/backup/. /tmp/social-sql-backup
|
||||
|
||||
restore-actors:
|
||||
docker cp /tmp/social-sql-backup/. $(call translate-container-name,$(strip $(DIR))_db_1):/tmp/backup
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_db_1) sh -c 'chown postgres /tmp/backup' && \
|
||||
docker exec -it $(call translate-container-name,$(strip $(DIR))_php_1) \
|
||||
sh -c "cd /var/www/social && bin/console doctrine:query:sql \"\
|
||||
copy actor from '/tmp/backup/actor.csv';\
|
||||
copy local_user from '/tmp/backup/local_user.csv';\
|
||||
copy local_group from '/tmp/backup/local_group.csv';\
|
||||
\
|
||||
copy activitypub_actor from '/tmp/backup/ap_actor.csv';\
|
||||
copy activitypub_rsa from '/tmp/backup/ap_rsa.csv';\
|
||||
\
|
||||
copy actor_subscription from '/tmp/backup/actor_subscription.csv';\
|
||||
copy group_member from '/tmp/backup/group_member.csv';\
|
||||
\
|
||||
copy feed from '/tmp/backup/feed.csv';\
|
||||
`cat /tmp/social-sql-backup/sequences`\""
|
||||
|
||||
force-nuke-everything: down remove-var remove-file up flush-redis-cache database-force-nuke install-plugins
|
||||
|
||||
force-delete-content: backup-actors force-nuke-everything restore-actors
|
||||
force-nuke-everything: down up flush-redis-cache database-force-nuke remove-var remove-file
|
||||
|
4
bin/configure
vendored
4
bin/configure
vendored
@@ -352,8 +352,8 @@ SOCIAL_DBMS=${DBMS}
|
||||
SOCIAL_DB=${DB_NAME}
|
||||
SOCIAL_USER=${DB_USER}
|
||||
SOCIAL_PASSWORD=${DB_PASSWORD}
|
||||
CONFIG_DOMAIN=${DOMAIN}
|
||||
CONFIG_NODE_NAME=${NODE_NAME}
|
||||
SOCIAL_DOMAIN=${DOMAIN}
|
||||
SOCIAL_NODE_NAME=${NODE_NAME}
|
||||
SOCIAL_ADMIN_EMAIL=${EMAIL}
|
||||
SOCIAL_SITE_PROFILE=${PROFILE}
|
||||
MAILER_DSN=${MAILER_DSN}
|
||||
|
@@ -20,23 +20,26 @@ const types = [
|
||||
'text' => 'string',
|
||||
'varchar' => 'string',
|
||||
'phone_number' => 'PhoneNumber',
|
||||
'float' => 'float', // TODO REMOVE THIS
|
||||
];
|
||||
|
||||
$files = array_merge(glob(ROOT . '/src/Entity/*.php'),
|
||||
array_merge(glob(ROOT . '/components/*/Entity/*.php'),
|
||||
glob(ROOT . '/plugins/*/Entity/*.php')));
|
||||
|
||||
$classes = [];
|
||||
$nullable_no_defaults_warning = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
|
||||
require_once $file;
|
||||
|
||||
$class = str_replace(['/', 'src', 'components', 'plugins'], ['\\', 'App', 'Component', 'Plugin'], substr($file, strlen(ROOT) + 1, -4));
|
||||
|
||||
if (!method_exists($class, 'schemaDef')) {
|
||||
continue;
|
||||
$declared = get_declared_classes();
|
||||
foreach ($declared as $dc) {
|
||||
if (preg_match('/(App|(Component|Plugin)\\\\[^\\\\]+)\\\\Entity/', $dc) && !in_array($dc, $classes)) {
|
||||
$class = $dc;
|
||||
$classes[] = $class;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$no_ns_class = preg_replace('/.*?\\\\/', '', $class);
|
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
for plugin in plugins/*; do
|
||||
install="${plugin}/bin/install.sh"
|
||||
if [ -x "${install}" ]; then
|
||||
( # subshell, to clear options/environment
|
||||
set -x
|
||||
"${install}"
|
||||
)
|
||||
fi
|
||||
done
|
10
codeception.yml
Normal file
10
codeception.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
paths:
|
||||
tests: tests/CodeCeption
|
||||
output: tests/CodeCeption/_output
|
||||
data: tests/CodeCeption/_data
|
||||
support: tests/CodeCeption/_support
|
||||
envs: tests/CodeCeption/_envs
|
||||
actor_suffix: Tester
|
||||
extensions:
|
||||
enabled:
|
||||
- Codeception\Extension\RunFailed
|
@@ -70,14 +70,7 @@ class Attachment extends Component
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
{
|
||||
if (!\in_array('attachment_to_note', $note_qb->getAllAliases())) {
|
||||
$note_qb->leftJoin(
|
||||
join: E\AttachmentToNote::class,
|
||||
alias: 'attachment_to_note',
|
||||
conditionType: Expr\Join::WITH,
|
||||
condition: 'note.id = attachment_to_note.note_id',
|
||||
);
|
||||
}
|
||||
$note_qb->leftJoin(E\AttachmentToNote::class, 'attachment_to_note', Expr\Join::WITH, 'note.id = attachment_to_note.note_id');
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,6 @@ use App\Util\Exception\NoSuchFileException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\Attachment\Entity\AttachmentThumbnail;
|
||||
use Component\Attachment\Entity\AttachmentToNote;
|
||||
use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -51,12 +50,7 @@ class Attachment extends Controller
|
||||
$attachment = DB::findOneBy('attachment', ['id' => $attachment_id]);
|
||||
$note = \is_int($note) ? Note::getById($note) : $note;
|
||||
|
||||
// Before anything, two very important things!
|
||||
// first: ensure this attachment is associated with this note
|
||||
if (DB::count(AttachmentToNote::class, ['attachment_id' => $attachment->getId(), 'note_id' => $note->getId()]) <= 0) {
|
||||
throw new ClientException(_m('No such attachment.'), 404);
|
||||
}
|
||||
// second: ensure proper scope
|
||||
// Before anything, ensure proper scope
|
||||
if (!$note->isVisibleTo(Common::actor())) {
|
||||
throw new ClientException(_m('You don\'t have permissions to view this attachment.'), 401);
|
||||
}
|
||||
@@ -95,7 +89,7 @@ class Attachment extends Controller
|
||||
try {
|
||||
return $this->attachment($attachment_id, $note_id, function ($res) use ($note_id, $attachment_id) {
|
||||
return [
|
||||
'_template' => 'attachment/view.html.twig',
|
||||
'_template' => '/cards/attachments/show.html.twig',
|
||||
'download' => $res['attachment']->getDownloadUrl(note: $note_id),
|
||||
'title' => $res['title'],
|
||||
'attachment' => $res['attachment'],
|
||||
@@ -151,18 +145,12 @@ class Attachment extends Controller
|
||||
*/
|
||||
public function attachmentThumbnailWithNote(Request $request, int $note_id, int $attachment_id, string $size = 'small'): Response
|
||||
{
|
||||
$attachment = DB::findOneBy('attachment', ['id' => $attachment_id]);
|
||||
$note = Note::getById($note_id);
|
||||
// Before anything, ensure proper scope
|
||||
if (!Note::getById($note_id)->isVisibleTo(Common::actor())) {
|
||||
throw new ClientException(_m('You don\'t have permissions to view this thumbnail.'), 401);
|
||||
}
|
||||
|
||||
// Before anything, two very important things!
|
||||
// first: ensure this attachment is associated with this note
|
||||
if (DB::count(AttachmentToNote::class, ['attachment_id' => $attachment->getId(), 'note_id' => $note->getId()]) <= 0) {
|
||||
throw new ClientException(_m('No such attachment.'), 404);
|
||||
}
|
||||
// second: ensure proper scope
|
||||
if (!$note->isVisibleTo(Common::actor())) {
|
||||
throw new ClientException(_m('You don\'t have permissions to view this attachment.'), 401);
|
||||
}
|
||||
$attachment = DB::findOneBy('attachment', ['id' => $attachment_id]);
|
||||
|
||||
$crop = Common::config('thumbnail', 'smart_crop');
|
||||
|
||||
|
@@ -35,7 +35,6 @@ use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\DuplicateFoundException;
|
||||
use App\Util\Exception\NoSuchFileException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use DateTimeInterface;
|
||||
@@ -224,7 +223,8 @@ class Attachment extends Entity
|
||||
$this->setFilename(null);
|
||||
$this->setSize(null);
|
||||
// Important not to null neither width nor height
|
||||
DB::wrapInTransaction(fn () => DB::persist($this));
|
||||
DB::persist($this);
|
||||
DB::flush();
|
||||
}
|
||||
} else {
|
||||
// @codeCoverageIgnoreStart
|
||||
@@ -339,11 +339,7 @@ class Attachment extends Entity
|
||||
*/
|
||||
public function getThumbnails()
|
||||
{
|
||||
return DB::findBy(
|
||||
AttachmentThumbnail::class,
|
||||
['attachment_id' => $this->id],
|
||||
order_by: ['size' => 'ASC', 'mimetype' => 'ASC'],
|
||||
);
|
||||
return DB::findBy('attachment_thumbnail', ['attachment_id' => $this->id]);
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
@@ -371,17 +367,15 @@ class Attachment extends Entity
|
||||
* @throws ClientException
|
||||
* @throws NotFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return AttachmentThumbnail
|
||||
*/
|
||||
public function getThumbnail(?string $size = null, bool $crop = false): ?AttachmentThumbnail
|
||||
{
|
||||
try {
|
||||
return AttachmentThumbnail::getOrCreate(attachment: $this, size: $size, crop: $crop);
|
||||
} catch (NoSuchFileException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getThumbnailUrl(Note|int $note, ?string $size = null): string
|
||||
public function getThumbnailUrl(Note|int $note, ?string $size = null)
|
||||
{
|
||||
return Router::url('note_attachment_thumbnail', ['note_id' => \is_int($note) ? $note : $note->getId(), 'attachment_id' => $this->getId(), 'size' => $size ?? Common::config('thumbnail', 'default_size')]);
|
||||
}
|
||||
|
@@ -30,7 +30,6 @@ use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
@@ -180,7 +179,7 @@ class AttachmentThumbnail extends Entity
|
||||
if (isset($this->attachment) && !\is_null($this->attachment)) {
|
||||
return $this->attachment;
|
||||
} else {
|
||||
return $this->attachment = DB::findOneBy(Attachment::class, ['id' => $this->attachment_id]);
|
||||
return $this->attachment = DB::findOneBy('attachment', ['id' => $this->attachment_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +204,7 @@ class AttachmentThumbnail extends Entity
|
||||
try {
|
||||
return Cache::get(
|
||||
self::getCacheKey($attachment->getId(), $size_int),
|
||||
fn () => DB::findOneBy(self::class, ['attachment_id' => $attachment->getId(), 'size' => $size_int]),
|
||||
fn () => DB::findOneBy('attachment_thumbnail', ['attachment_id' => $attachment->getId(), 'size' => $size_int]),
|
||||
);
|
||||
} catch (NotFoundException) {
|
||||
if (\is_null($attachment->getWidth()) || \is_null($attachment->getHeight())) {
|
||||
@@ -214,7 +213,7 @@ class AttachmentThumbnail extends Entity
|
||||
[$predicted_width, $predicted_height] = self::predictScalingValues($attachment->getWidth(), $attachment->getHeight(), $size, $crop);
|
||||
if (\is_null($attachment->getPath()) || !file_exists($attachment->getPath())) {
|
||||
// Before we quit, check if there's any other thumb
|
||||
$alternative_thumbs = DB::findBy(self::class, ['attachment_id' => $attachment->getId()]);
|
||||
$alternative_thumbs = DB::findBy('attachment_thumbnail', ['attachment_id' => $attachment->getId()]);
|
||||
usort($alternative_thumbs, fn ($l, $r) => $r->getSize() <=> $l->getSize());
|
||||
if (empty($alternative_thumbs)) {
|
||||
throw new NotStoredLocallyException();
|
||||
@@ -254,14 +253,14 @@ class AttachmentThumbnail extends Entity
|
||||
}
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
public function getPath()
|
||||
{
|
||||
return Common::config('thumbnail', 'dir') . \DIRECTORY_SEPARATOR . $this->getFilename();
|
||||
}
|
||||
|
||||
public function getUrl(Note|int $note): string
|
||||
public function getUrl()
|
||||
{
|
||||
return Router::url('note_attachment_thumbnail', ['note_id' => \is_int($note) ? $note : $note->getId(), 'attachment_id' => $this->getAttachmentId(), 'size' => self::sizeIntToStr($this->getSize())]);
|
||||
return Router::url('attachment_thumbnail', ['id' => $this->getAttachmentId(), 'size' => self::sizeIntToStr($this->getSize())]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,10 +277,9 @@ class AttachmentThumbnail extends Entity
|
||||
}
|
||||
}
|
||||
Cache::delete(self::getCacheKey($this->getAttachmentId(), $this->getSize()));
|
||||
if ($flush) {
|
||||
DB::wrapInTransaction(fn () => DB::remove($this));
|
||||
} else {
|
||||
DB::remove($this);
|
||||
if ($flush) {
|
||||
DB::flush();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,166 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Blog\Controller;
|
||||
|
||||
use App\Core\ActorLocalRoles;
|
||||
use App\Core\Controller;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Form\FormFields;
|
||||
use Component\Posting\Posting;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
|
||||
class Post extends Controller
|
||||
{
|
||||
/**
|
||||
* Creates and handles Blog post creation form
|
||||
*
|
||||
* @throws \App\Util\Exception\DuplicateFoundException
|
||||
* @throws \App\Util\Exception\NoLoggedInUser
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
* @throws ClientException
|
||||
* @throws RedirectException
|
||||
*/
|
||||
public function makePost(Request $request)
|
||||
{
|
||||
$actor = Common::ensureLoggedIn()->getActor();
|
||||
|
||||
$placeholder_strings = ['How are you feeling?', 'Have something to share?', 'How was your day?'];
|
||||
Event::handle('PostingPlaceHolderString', [&$placeholder_strings]);
|
||||
$placeholder = $placeholder_strings[array_rand($placeholder_strings)];
|
||||
|
||||
$initial_content = '';
|
||||
Event::handle('PostingInitialContent', [&$initial_content]);
|
||||
|
||||
$available_content_types = [
|
||||
_m('Plain Text') => 'text/plain',
|
||||
];
|
||||
Event::handle('PostingAvailableContentTypes', [&$available_content_types]);
|
||||
|
||||
if (!\is_int($this->int('in'))) {
|
||||
throw new InvalidArgumentException('You must specify an In group/org.');
|
||||
}
|
||||
$context_actor = Actor::getById($this->int('in'));
|
||||
if (!$context_actor->isGroup()) {
|
||||
throw new InvalidArgumentException('Only group blog posts are supported for now.');
|
||||
}
|
||||
$in_targets = ["!{$context_actor->getNickname()}" => $context_actor->getId()];
|
||||
$form_params[] = ['in', ChoiceType::class, ['label' => _m('In:'), 'multiple' => false, 'expanded' => false, 'choices' => $in_targets]];
|
||||
|
||||
$visibility_options = [
|
||||
_m('Public') => VisibilityScope::EVERYWHERE->value,
|
||||
_m('Local') => VisibilityScope::LOCAL->value,
|
||||
_m('Addressee') => VisibilityScope::ADDRESSEE->value,
|
||||
];
|
||||
if (!\is_null($context_actor) && $context_actor->isGroup()) { // @phpstan-ignore-line currently an open bug. See https://web.archive.org/web/20220226131651/https://github.com/phpstan/phpstan/issues/6234
|
||||
if ($actor->canModerate($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, 'choices' => $visibility_options]];
|
||||
|
||||
$form_params[] = ['title', TextType::class, ['label' => _m('Title:'), 'constraints' => [new Length(['max' => 129])], 'required' => true]];
|
||||
$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) {
|
||||
$form_params[] = ['content_type', ChoiceType::class,
|
||||
[
|
||||
'label' => _m('Text format:'), 'multiple' => false, 'expanded' => false,
|
||||
'data' => $available_content_types[array_key_first($available_content_types)],
|
||||
'choices' => $available_content_types,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Event::handle('PostingAddFormEntries', [$request, $actor, &$form_params]);
|
||||
|
||||
$form_params[] = ['post_note', SubmitType::class, ['label' => _m('Post')]];
|
||||
$form = Form::create($form_params);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted()) {
|
||||
try {
|
||||
if ($form->isValid()) {
|
||||
$data = $form->getData();
|
||||
Event::handle('PostingModifyData', [$request, $actor, &$data, $form_params, $form]);
|
||||
|
||||
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.'));
|
||||
}
|
||||
|
||||
if (\is_null(VisibilityScope::tryFrom($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)];
|
||||
$extra_args = [];
|
||||
Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form_params, $form]);
|
||||
|
||||
[,$note,] = Posting::storeLocalPage(
|
||||
actor: $actor,
|
||||
content: $data['content'],
|
||||
content_type: $content_type,
|
||||
locale: $data['language'],
|
||||
scope: VisibilityScope::from($data['visibility']),
|
||||
targets: [(int) $data['in']],
|
||||
reply_to: $data['reply_to_id'],
|
||||
attachments: $data['attachments'],
|
||||
process_note_content_extra_args: $extra_args,
|
||||
title: $data['title'],
|
||||
);
|
||||
|
||||
return new RedirectResponse($note->getConversationUrl());
|
||||
}
|
||||
} catch (FormSizeFileException $e) {
|
||||
throw new ClientException(_m('Invalid file size given'), previous: $e);
|
||||
}
|
||||
}
|
||||
return [
|
||||
'_template' => 'blog/make_post.html.twig',
|
||||
'blog_entry_form' => $form->createView(),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
{% extends 'stdgrid.html.twig' %}
|
||||
{% block title %}{% trans %}Create a blog post{% endtrans %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{{ parent() }}
|
||||
<section class="frame-section frame-section-padding">
|
||||
<h1>{% trans %}Create a blog post{% endtrans %}</h1>
|
||||
{{ form(blog_entry_form) }}
|
||||
</section>
|
||||
{% endblock body %}
|
@@ -57,8 +57,8 @@ class Circle extends Component
|
||||
{
|
||||
use MetaCollectionTrait;
|
||||
public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/';
|
||||
protected const SLUG = 'circle';
|
||||
protected const PLURAL_SLUG = 'circles';
|
||||
protected string $slug = 'circle';
|
||||
protected string $plural_slug = 'circles';
|
||||
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
{
|
||||
@@ -97,7 +97,7 @@ class Circle extends Component
|
||||
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
|
||||
{
|
||||
if ($section === 'profile' && \in_array($request->get('_route'), ['person_actor_settings', 'group_actor_settings'])) {
|
||||
if ($section === 'profile' && $request->get('_route') === 'settings') {
|
||||
$tabs[] = [
|
||||
'title' => 'Self tags',
|
||||
'desc' => 'Add or remove tags on yourself',
|
||||
@@ -195,17 +195,6 @@ class Circle extends Component
|
||||
return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of Collections owned by an Actor.
|
||||
* In this case, Collections of those within Actor's own circle of Actors, aka ActorCircle.
|
||||
*
|
||||
* Differs from the overwritten method in MetaCollectionsTrait, since retrieved Collections come from the $owner
|
||||
* 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 bool $ids_only true if only the Collections ids are to be returned
|
||||
*/
|
||||
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
|
||||
{
|
||||
$tagged_id = !\is_null($vars) ? $this->getActorIdFromVars($vars) : null;
|
||||
|
@@ -31,14 +31,6 @@ use Component\Collection\Util\Controller\CircleController;
|
||||
|
||||
class Circle extends CircleController
|
||||
{
|
||||
/**
|
||||
* Render an existing ActorCircle with the given id as a Collection of Actors
|
||||
*
|
||||
* @param ActorCircle|int $circle_id the desired ActorCircle id
|
||||
*
|
||||
* @throws \App\Util\Exception\ServerException
|
||||
* @throws ClientException
|
||||
*/
|
||||
public function circleById(int|ActorCircle $circle_id): array
|
||||
{
|
||||
$circle = \is_int($circle_id) ? ActorCircle::getByPK(['id' => $circle_id]) : $circle_id;
|
||||
|
@@ -33,8 +33,8 @@ use Component\Collection\Util\Controller\MetaCollectionController;
|
||||
|
||||
class Circles extends MetaCollectionController
|
||||
{
|
||||
protected const SLUG = 'circle';
|
||||
protected const PLURAL_SLUG = 'circles';
|
||||
protected string $slug = 'circle';
|
||||
protected string $plural_slug = 'circles';
|
||||
protected string $page_title = 'Actor circles';
|
||||
|
||||
public function createCollection(int $owner_id, string $name)
|
||||
|
@@ -23,12 +23,11 @@ class SelfTagsSettings extends Controller
|
||||
{
|
||||
/**
|
||||
* Generic settings page for an Actor's self tags
|
||||
* TODO: We should have $actor->setSelfTags(), $actor->addSelfTags(), $actor->removeSelfTags()
|
||||
*/
|
||||
public static function settingsSelfTags(Request $request, E\Actor $target, string $details_id)
|
||||
{
|
||||
$actor = Common::actor();
|
||||
if (!$actor->canModerate($target)) {
|
||||
if (!$actor->canAdmin($target)) {
|
||||
throw new ClientException(_m('You don\'t have enough permissions to edit {nickname}\'s settings', ['{nickname}' => $target->getNickname()]));
|
||||
}
|
||||
|
||||
|
@@ -49,7 +49,7 @@ class ActorCircle extends Entity
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $id;
|
||||
private ?int $tagger = null;
|
||||
private ?int $tagger = null; // If null, is the special global self-tag circle
|
||||
private string $tag;
|
||||
private ?string $description = null;
|
||||
private ?bool $private = false;
|
||||
@@ -202,7 +202,7 @@ class ActorCircle extends Entity
|
||||
'description' => 'An actor can have lists of actors, to separate their feed or quickly mention his friend',
|
||||
'fields' => [
|
||||
'id' => ['type' => 'serial', 'not null' => true, 'description' => 'unique identifier'], // An actor can be tagged by many actors
|
||||
'tagger' => ['type' => 'int', 'default' => null, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'description' => 'user making the tag, null if self-tag. If null, is the special global self-tag circle'],
|
||||
'tagger' => ['type' => 'int', 'default' => null, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'actor_list_tagger_fkey', 'description' => 'user making the tag, null if self-tag'],
|
||||
'tag' => ['type' => 'varchar', 'length' => 64, 'foreign key' => true, 'target' => 'ActorTag.tag', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'actor tag'], // Join with ActorTag // // so, Doctrine doesn't like that the target is not unique, even though the pair is // Many Actor Circles can reference (and probably will) an Actor Tag
|
||||
'description' => ['type' => 'text', 'description' => 'description of the people tag'],
|
||||
'private' => ['type' => 'bool', 'default' => false, 'description' => 'is this tag private'],
|
||||
|
@@ -23,31 +23,20 @@ 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
|
||||
*/
|
||||
public static function query(string $query, int $page, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
|
||||
public static function query(string $query, int $page, ?string $locale = null, ?Actor $actor = null): array
|
||||
{
|
||||
$note_criteria = null;
|
||||
$actor_criteria = null;
|
||||
if (!empty($query = trim($query))) {
|
||||
[$note_criteria, $actor_criteria] = Parser::parse($query, $locale, $actor);
|
||||
}
|
||||
|
||||
$note_qb = DB::createQueryBuilder();
|
||||
$actor_qb = DB::createQueryBuilder();
|
||||
// TODO consider selecting note related stuff, to avoid separate queries (though they're cached, so maybe it's okay)
|
||||
$note_qb->select('note')->from('App\Entity\Note', 'note');
|
||||
$actor_qb->select('actor')->from('App\Entity\Actor', 'actor');
|
||||
$note_qb->select('note')->from('App\Entity\Note', 'note')->orderBy('note.created', 'DESC')->addOrderBy('note.id', 'DESC');
|
||||
$actor_qb->select('actor')->from('App\Entity\Actor', 'actor')->orderBy('actor.created', 'DESC')->addOrderBy('actor.id', 'DESC');
|
||||
Event::handle('CollectionQueryAddJoins', [&$note_qb, &$actor_qb, $note_criteria, $actor_criteria]);
|
||||
|
||||
// Handle ordering
|
||||
$note_order_by = !empty($note_order_by) ? $note_order_by : ['note.created' => 'DESC', 'note.id' => 'DESC'];
|
||||
$actor_order_by = !empty($actor_order_by) ? $actor_order_by : ['actor.created' => 'DESC', 'actor.id' => 'DESC'];
|
||||
foreach ($note_order_by as $field => $order) {
|
||||
$note_qb->addOrderBy($field, $order);
|
||||
}
|
||||
foreach ($actor_order_by as $field => $order) {
|
||||
$actor_qb->addOrderBy($field, $order);
|
||||
}
|
||||
|
||||
$notes = [];
|
||||
$actors = [];
|
||||
if (!\is_null($note_criteria)) {
|
||||
@@ -66,13 +55,8 @@ class Collection extends Component
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
{
|
||||
$note_aliases = $note_qb->getAllAliases();
|
||||
if (!\in_array('subscription', $note_aliases)) {
|
||||
$note_qb->leftJoin(ActorSubscription::class, 'subscription', Expr\Join::WITH, 'note.actor_id = subscription.subscribed_id');
|
||||
}
|
||||
if (!\in_array('note_actor', $note_aliases)) {
|
||||
$note_qb->leftJoin(Actor::class, 'note_actor', Expr\Join::WITH, 'note.actor_id = note_actor.id');
|
||||
}
|
||||
$note_qb->leftJoin(ActorSubscription::class, 'subscription', Expr\Join::WITH, 'note.actor_id = subscription.subscribed_id')
|
||||
->leftJoin(Actor::class, 'note_actor', Expr\Join::WITH, 'note.actor_id = note_actor.id');
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
@@ -120,7 +104,7 @@ class Collection extends Component
|
||||
if (\in_array($type, ['actor', 'actors'])) {
|
||||
$type_consts = null;
|
||||
} else {
|
||||
$type_consts[] = \constant(Actor::class . '::' . mb_strtoupper($type === 'organisation' ? 'group' : $type));
|
||||
$type_consts[] = \constant(Actor::class . '::' . mb_strtoupper($type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +127,8 @@ class Collection extends Component
|
||||
foreach (
|
||||
[
|
||||
Actor::PERSON => ['person', 'people'],
|
||||
Actor::GROUP => ['group', 'groups', 'org', 'orgs', 'organisation', 'organisations', 'organization', 'organizations'],
|
||||
Actor::GROUP => ['group', 'groups'],
|
||||
Actor::ORGANISATION => ['org', 'orgs', 'organization', 'organizations', 'organisation', 'organisations'],
|
||||
Actor::BOT => ['bot', 'bots'],
|
||||
] as $type => $match) {
|
||||
if (array_intersect(explode(',', $term[1]), $match) !== []) {
|
||||
|
71
components/Collection/Util/ActorControllerTrait.php
Normal file
71
components/Collection/Util/ActorControllerTrait.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
/**
|
||||
* Base class for feed controllers
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @category Controller
|
||||
*
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
|
||||
namespace Component\Collection\Util;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Util\Exception\ClientException;
|
||||
|
||||
trait ActorControllerTrait
|
||||
{
|
||||
/**
|
||||
* Generic function that handles getting a representation for an actor from id
|
||||
*/
|
||||
protected function handleActorById(int $id, callable $handle)
|
||||
{
|
||||
$actor = DB::findOneBy('actor', ['id' => $id]);
|
||||
if ($actor->getIsLocal()) {
|
||||
return ['_redirect' => $actor->getUrl(Router::ABSOLUTE_PATH), 'actor' => $actor];
|
||||
}
|
||||
if (empty($actor)) {
|
||||
throw new ClientException(_m('No such actor.'), 404);
|
||||
} else {
|
||||
return $handle($actor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic function that handles getting a representation for an actor from nickname
|
||||
*/
|
||||
protected function handleActorByNickname(string $nickname, callable $handle)
|
||||
{
|
||||
$user = DB::findOneBy('local_user', ['nickname' => $nickname]);
|
||||
$actor = DB::findOneBy('actor', ['id' => $user->getId()]);
|
||||
if (empty($actor)) {
|
||||
throw new ClientException(_m('No such actor.'), 404);
|
||||
} else {
|
||||
return $handle($actor);
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,10 +11,10 @@ use Component\Collection\Collection as CollectionModule;
|
||||
|
||||
class Collection extends Controller
|
||||
{
|
||||
public function query(string $query, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
|
||||
public function query(string $query, ?string $locale = null, ?Actor $actor = null): 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 CollectionModule::query($query, $this->int('page') ?? 1, $locale, $actor);
|
||||
}
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@ abstract class FeedController extends OrderedCollection
|
||||
$actor = Common::actor();
|
||||
if (\array_key_exists('notes', $result)) {
|
||||
$notes = $result['notes'];
|
||||
self::enforceScope($notes, $actor, $result['actor'] ?? null);
|
||||
self::enforceScope($notes, $actor);
|
||||
Event::handle('FilterNoteList', [$actor, &$notes, $result['request']]);
|
||||
Event::handle('FormatNoteList', [$notes, &$result['notes'], &$result['request']]);
|
||||
}
|
||||
@@ -58,8 +58,8 @@ abstract class FeedController extends OrderedCollection
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function enforceScope(array &$notes, ?Actor $actor, ?Actor $in = null): void
|
||||
private static function enforceScope(array &$notes, ?Actor $actor): void
|
||||
{
|
||||
$notes = F\select($notes, fn (Note $n) => $n->isVisibleTo($actor, $in));
|
||||
$notes = F\select($notes, fn (Note $n) => $n->isVisibleTo($actor));
|
||||
}
|
||||
}
|
||||
|
@@ -43,8 +43,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
abstract class MetaCollectionController extends FeedController
|
||||
{
|
||||
protected const SLUG = 'collectionsEntry';
|
||||
protected const PLURAL_SLUG = 'collectionsList';
|
||||
protected string $slug = 'collectionsEntry';
|
||||
protected string $plural_slug = 'collectionsList';
|
||||
protected string $page_title = 'Collections';
|
||||
|
||||
abstract public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string;
|
||||
@@ -76,8 +76,8 @@ abstract class MetaCollectionController extends FeedController
|
||||
{
|
||||
$collections = $this->getCollectionsByActorId($id);
|
||||
|
||||
$create_title = _m('Create a ' . mb_strtolower(preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', static::SLUG)));
|
||||
$collections_title = _m('The ' . mb_strtolower(preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', static::PLURAL_SLUG)));
|
||||
$create_title = _m('Create a ' . mb_strtolower(preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', $this->slug)));
|
||||
$collections_title = _m('The ' . mb_strtolower(preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', $this->plural_slug)));
|
||||
// create collection form
|
||||
$create = null;
|
||||
if (Common::user()?->getId() === $id) {
|
||||
@@ -111,8 +111,8 @@ abstract class MetaCollectionController extends FeedController
|
||||
//
|
||||
// Instead, I'm using an anonymous class to encapsulate
|
||||
// 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) {
|
||||
// This is suggested at https://stackoverflow.com/a/50364502.
|
||||
$fn = new class($id, $nickname, $request, $this, $this->slug) {
|
||||
private $id;
|
||||
private $nick;
|
||||
private $request;
|
||||
|
@@ -46,8 +46,8 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
trait MetaCollectionTrait
|
||||
{
|
||||
//protected const SLUG = 'collection';
|
||||
//protected const PLURAL_SLUG = 'collections';
|
||||
//protected string $slug = 'collection';
|
||||
//protected string $plural_slug = 'collections';
|
||||
|
||||
/**
|
||||
* create a collection owned by Actor $owner.
|
||||
@@ -127,9 +127,9 @@ trait MetaCollectionTrait
|
||||
},
|
||||
]],
|
||||
['add', SubmitType::class, [
|
||||
'label' => _m('Add to ' . static::PLURAL_SLUG),
|
||||
'label' => _m('Add to ' . $this->plural_slug),
|
||||
'attr' => [
|
||||
'title' => _m('Add to ' . static::PLURAL_SLUG),
|
||||
'title' => _m('Add to ' . $this->plural_slug),
|
||||
],
|
||||
]],
|
||||
]);
|
||||
@@ -151,17 +151,17 @@ trait MetaCollectionTrait
|
||||
// form: add to new collection
|
||||
$create_form = Form::create([
|
||||
['name', TextType::class, [
|
||||
'label' => _m('Add to a new ' . static::SLUG),
|
||||
'label' => _m('Add to a new ' . $this->slug),
|
||||
'attr' => [
|
||||
'placeholder' => _m('New ' . static::SLUG . ' name'),
|
||||
'placeholder' => _m('New ' . $this->slug . ' name'),
|
||||
'required' => 'required',
|
||||
],
|
||||
'data' => '',
|
||||
]],
|
||||
['create', SubmitType::class, [
|
||||
'label' => _m('Create a new ' . static::SLUG),
|
||||
'label' => _m('Create a new ' . $this->slug),
|
||||
'attr' => [
|
||||
'title' => _m('Create a new ' . static::SLUG),
|
||||
'title' => _m('Create a new ' . $this->slug),
|
||||
],
|
||||
]],
|
||||
]);
|
||||
@@ -176,7 +176,7 @@ trait MetaCollectionTrait
|
||||
$res[] = Formatting::twigRenderFile(
|
||||
'collection/widget_add_to.html.twig',
|
||||
[
|
||||
'ctitle' => _m('Add to ' . static::PLURAL_SLUG),
|
||||
'ctitle' => _m('Add to ' . $this->plural_slug),
|
||||
'user' => $user,
|
||||
'has_collections' => \count($collections) > 0,
|
||||
'add_form' => $add_form->createView(),
|
||||
|
@@ -50,9 +50,8 @@ abstract class Parser
|
||||
* recognises either spaces (currently `or`, should be fuzzy match), `OR` or `|` (`or`) and `AND` or `&` (`and`)
|
||||
*
|
||||
* TODO: Better fuzzy match, implement exact match with quotes and nesting with parens
|
||||
* TODO: Proper parser, tokenize better. Mostly a rewrite
|
||||
*
|
||||
* @return array{?Criteria, ?Criteria} [?$note_criteria, ?$actor_criteria]
|
||||
* @return Criteria[]
|
||||
*/
|
||||
public static function parse(string $input, ?string $locale = null, ?Actor $actor = null, int $level = 0): array
|
||||
{
|
||||
@@ -81,16 +80,15 @@ abstract class Parser
|
||||
$actor_res = null;
|
||||
Event::handle('CollectionQueryCreateExpression', [$eb, $term, $locale, $actor, &$note_res, &$actor_res]);
|
||||
if (\is_null($note_res) && \is_null($actor_res)) { // @phpstan-ignore-line
|
||||
//throw new ServerException("No one claimed responsibility for a match term: {$term}");
|
||||
// It's okay if the term doesn't exist, just perform a regular search
|
||||
throw new ServerException("No one claimed responsibility for a match term: {$term}");
|
||||
}
|
||||
if (!empty($note_res)) { // @phpstan-ignore-line currently an open bug. See https://web.archive.org/web/20220226131651/https://github.com/phpstan/phpstan/issues/6234
|
||||
if (!empty($note_res)) { // @phpstan-ignore-line
|
||||
if (\is_array($note_res)) {
|
||||
$note_res = $eb->orX(...$note_res);
|
||||
}
|
||||
$note_parts[] = $note_res;
|
||||
}
|
||||
if (!empty($actor_res)) { // @phpstan-ignore-line currently an open bug. See https://web.archive.org/web/20220226131651/https://github.com/phpstan/phpstan/issues/6234
|
||||
if (!empty($actor_res)) {
|
||||
if (\is_array($actor_res)) {
|
||||
$actor_res = $eb->orX(...$actor_res);
|
||||
}
|
||||
@@ -109,18 +107,18 @@ abstract class Parser
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
if (!$match) {
|
||||
if (!$match) { // @phpstan-ignore-line
|
||||
++$right;
|
||||
}
|
||||
}
|
||||
|
||||
$note_criteria = null;
|
||||
$actor_criteria = null;
|
||||
if (!empty($note_parts)) {
|
||||
if (!empty($note_parts)) { // @phpstan-ignore-line
|
||||
self::connectParts($note_parts, $note_criteria_arr, $last_op, $eb, force: true);
|
||||
$note_criteria = new Criteria($eb->orX(...$note_criteria_arr));
|
||||
}
|
||||
if (!empty($actor_parts)) { // @phpstan-ignore-line weird, but this whole thing needs a rewrite
|
||||
if (!empty($actor_parts)) { // @phpstan-ignore-line
|
||||
self::connectParts($actor_parts, $actor_criteria_arr, $last_op, $eb, force: true);
|
||||
$actor_criteria = new Criteria($eb->orX(...$actor_criteria_arr));
|
||||
}
|
||||
|
@@ -3,17 +3,8 @@
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<section class="frame-section frame-section-padding">
|
||||
<header class="feed-header">
|
||||
{% if actors_feed_title is defined %}
|
||||
{{ actors_feed_title.getHtml() }}
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% set prepend_actors_collection = handle_event('PrependActorsCollection', request) %}
|
||||
{% for widget in prepend_actors_collection %}
|
||||
{{ widget | raw }}
|
||||
{% endfor %}
|
||||
<div class="frame-section frame-section-padding">
|
||||
<h1 class="frame-section-title">{{ title }}</h1>
|
||||
|
||||
<details class="frame-section section-details-title">
|
||||
<summary class="details-summary-title">
|
||||
@@ -54,17 +45,16 @@
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<section class="frame-section frame-section-padding">
|
||||
<h2>{% trans %}Results{% endtrans %}</h2>
|
||||
<section class="frame-section-padding">
|
||||
{% if actors is defined and actors is not empty %}
|
||||
{% for actor in actors %}
|
||||
{% block profile_view %}{% include 'cards/blocks/profile.html.twig' %}{% endblock profile_view %}
|
||||
{% block profile_view %}{% include 'cards/profile/view.html.twig' %}{% endblock profile_view %}
|
||||
<hr>
|
||||
{% endfor %}
|
||||
<span class="frame-section-button-like">{% trans %}Page: %page%{% endtrans %}</span>
|
||||
<p>{% trans %}Page: %page%{% endtrans %}</p>
|
||||
{% else %}
|
||||
<span>{{ empty_message }}</span>
|
||||
<h2>{{ empty_message }}</h2>
|
||||
{% endif %}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{% extends '/collection/notes.html.twig' %}
|
||||
|
||||
{% block title %}{% trans %}%page_title%{% endtrans %}{% endblock %}
|
||||
{% block title %}{{ page_title | trans }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="frame-section frame-section-padding">
|
||||
<h2 class="frame-section-title">{% trans %}%page_title%{% endtrans %}</h2>
|
||||
<h2 class="frame-section-title">{{ page_title | trans }}</h2>
|
||||
{% block collection_items %}
|
||||
{% endblock collection_items %}
|
||||
</div>
|
||||
|
@@ -1,17 +1,17 @@
|
||||
{% extends 'stdgrid.html.twig' %}
|
||||
|
||||
{% block title %}{% trans %}%page_title%{% endtrans %}{% endblock %}
|
||||
{% block title %}{{ page_title | trans }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="frame-section frame-section-padding">
|
||||
<h2 class="frame-section-title">{% trans %}%page_title%{% endtrans %}</h2>
|
||||
<h2 class="frame-section-title">{{ page_title | trans }}</h2>
|
||||
{% if add_collection %}
|
||||
<div class="frame-section section-form">
|
||||
{{ form(add_collection) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="frame-section collections-list">
|
||||
<h3>{% trans %}%list_title%{% endtrans %}</h3>
|
||||
<h3>{{ list_title | trans }}</h3>
|
||||
{% for col in collections %}
|
||||
<div class="collection-item">
|
||||
<a class="name" href="{{ fn.getUrl(col.id) }}">{{ col.name }}</a>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
{% extends 'stdgrid.html.twig' %}
|
||||
{% import '/cards/macros/note/factory.html.twig' as NoteFactory %}
|
||||
{% import '/cards/note/view.html.twig' as noteView %}
|
||||
|
||||
{% block title %}{% if page_title is defined %}{% trans %}%page_title%{% endtrans %}{% endif %}{% endblock %}
|
||||
{% block title %}{% if page_title is defined %}{{ page_title | trans }}{% endif %}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/feeds.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block body %}
|
||||
@@ -15,44 +15,41 @@
|
||||
|
||||
{% if notes is defined %}
|
||||
<header class="feed-header">
|
||||
{% set current_path = app.request.get('_route') %}
|
||||
{% if notes_feed_title is defined %}
|
||||
{{ notes_feed_title.getHtml() }}
|
||||
{% if page_title is defined %}
|
||||
<h1 class="heading-no-margin">{{ page_title | trans }}</h1>
|
||||
{% else %}
|
||||
<h3 class="heading-no-margin">{{ 'Notes' | trans }}</h3>
|
||||
{% endif %}
|
||||
<nav class="feed-actions" title="{% trans %}Actions that change how the feed behaves{% endtrans %}">
|
||||
<details class="feed-actions-details" role="group">
|
||||
<nav class="feed-actions">
|
||||
<details class="feed-actions-details">
|
||||
<summary>
|
||||
{{ icon('filter', 'icon icon-feed-actions') | raw }} {# button-container #}
|
||||
</summary>
|
||||
<menu class="feed-actions-details-dropdown" role="toolbar">
|
||||
<div class="feed-actions-details-dropdown">
|
||||
<menu>
|
||||
{% for block in handle_event('AddFeedActions', app.request, notes is defined and notes is not empty) %}
|
||||
{{ block | raw }}
|
||||
{% endfor %}
|
||||
</menu>
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{% if notes is not empty %}
|
||||
{# Backwards compatibility with hAtom 0.1 #}
|
||||
<section class="feed h-feed hfeed notes" role="feed" aria-busy="false" title="{% trans %}Feed content{% endtrans %}">
|
||||
<section class="feed h-feed hfeed notes" tabindex="0" role="feed">
|
||||
{% for conversation in notes %}
|
||||
{% block current_note %}
|
||||
{% if conversation is instanceof('array') %}
|
||||
{% set args = conversation | merge({'type': 'vanilla_full'}) %}
|
||||
{{ NoteFactory.constructor(args) }}
|
||||
{# {% else %}
|
||||
{% set args = { 'type': 'vanilla_full', 'note': conversation, 'extra': { 'depth': 0 } } %}
|
||||
{{ NoteFactory.constructor(args) }}#}
|
||||
{{ noteView.macro_note(conversation['note'], conversation['replies']) }}
|
||||
{% else %}
|
||||
{{ noteView.macro_note(conversation) }}
|
||||
{% endif %}
|
||||
<hr class="hr-replies-end" role="separator" aria-label="{% trans %}Marks the end of previous conversation's initial note{% endtrans %}">
|
||||
<hr tabindex="0" title="{{ 'End of note and replies.' | trans }}">
|
||||
{% endblock current_note %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="feed h-feed hfeed notes" tabindex="0" role="feed">
|
||||
<span>{% trans %}No notes here...{% endtrans %}</span>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock body %}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<section class="frame-section collections">
|
||||
<details class="section-details-title" title="Expand if you want to access more options.">
|
||||
<summary class="details-summary-title">
|
||||
<span>{{ctitle}}</span>
|
||||
<h2>{{ctitle}}</h2>
|
||||
</summary>
|
||||
{% if has_collections %}
|
||||
<section class="section-form">
|
||||
|
@@ -40,7 +40,6 @@ use App\Util\Exception\NoLoggedInUser;
|
||||
use App\Util\Exception\NoSuchNoteException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Component\Conversation\Entity\ConversationMute;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@@ -59,17 +58,11 @@ class Conversation extends FeedController
|
||||
*/
|
||||
public function showConversation(Request $request, int $conversation_id): array
|
||||
{
|
||||
$page_title = _m('Conversation');
|
||||
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'notes' => $this->query(
|
||||
query: "note-conversation:{$conversation_id}",
|
||||
note_order_by: ['note.created' => 'ASC', 'note.id' => 'ASC'],
|
||||
)['notes'] ?? [],
|
||||
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
|
||||
'should_format' => false,
|
||||
'page_title' => $page_title,
|
||||
'notes_feed_title' => (new Heading(1, [], $page_title)),
|
||||
'page_title' => _m('Conversation'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -141,10 +134,7 @@ class Conversation extends FeedController
|
||||
|
||||
return [
|
||||
'_template' => 'conversation/mute.html.twig',
|
||||
'notes' => $this->query(
|
||||
query: "note-conversation:{$conversation_id}",
|
||||
note_order_by: ['note.created' => 'ASC', 'note.id' => 'ASC'],
|
||||
)['notes'] ?? [],
|
||||
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
|
||||
'is_muted' => $is_muted,
|
||||
'form' => $form->createView(),
|
||||
];
|
||||
|
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
@@ -16,14 +18,8 @@ declare(strict_types = 1);
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
/**
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @author Eliseu Amaro <mail@eliseuama.ro>
|
||||
* @copyright 2021-2022 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
// }}}
|
||||
|
||||
namespace Component\Conversation;
|
||||
|
||||
@@ -38,7 +34,6 @@ use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Formatting;
|
||||
use Component\Conversation\Entity\Conversation as ConversationEntity;
|
||||
use Component\Conversation\Entity\ConversationMute;
|
||||
use Functional as F;
|
||||
@@ -83,12 +78,14 @@ class Conversation extends Component
|
||||
} else {
|
||||
// It's a reply for sure
|
||||
// Set reply_to property in newly created Note to parent's id
|
||||
$current_note->setReplyTo($parent_id);
|
||||
|
||||
// Parent will have a conversation of its own, the reply should have the same one
|
||||
$parent_note = Note::getById($parent_id);
|
||||
$current_note->setConversationId($parent_note->getConversationId());
|
||||
}
|
||||
|
||||
DB::persist($current_note);
|
||||
DB::merge($current_note);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,8 +99,6 @@ class Conversation extends Component
|
||||
* '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
|
||||
{
|
||||
@@ -119,8 +114,7 @@ class Conversation extends Component
|
||||
'conversation_reply_to',
|
||||
[
|
||||
'reply_to_id' => $note->getId(),
|
||||
'from' => $from,
|
||||
'_fragment' => 'note-anchor-' . $note->getId(),
|
||||
'from' => $from . '#note-anchor-' . $note->getId(),
|
||||
],
|
||||
Router::ABSOLUTE_PATH,
|
||||
);
|
||||
@@ -137,20 +131,29 @@ class Conversation extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posting event to add extra info to a note
|
||||
*/
|
||||
public function onPostingModifyData(Request $request, Actor $actor, array &$data): bool
|
||||
{
|
||||
$data['reply_to_id'] = $request->get('_route') === 'conversation_reply_to' && $request->query->has('reply_to_id')
|
||||
? $request->query->getInt('reply_to_id')
|
||||
: null;
|
||||
|
||||
if (!\is_null($data['reply_to_id'])) {
|
||||
Note::ensureCanInteract(Note::getById($data['reply_to_id']), $actor);
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append on note information about user actions.
|
||||
*
|
||||
* @param array $vars Contains information related to Note currently being rendered
|
||||
* @param array $result Contains keys 'actors', and 'action'. Needed to construct a string, stating who ($result['actors']), has already performed a reply ($result['action']), in the given Note (vars['note'])
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onAppendCardNote(array $vars, array &$result): bool
|
||||
{
|
||||
if (str_contains($vars['request']->getPathInfo(), 'conversation')) {
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
// The current Note being rendered
|
||||
$note = $vars['note'];
|
||||
|
||||
@@ -172,22 +175,6 @@ class Conversation extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
private function getReplyToIdFromRequest(Request $request): ?int
|
||||
{
|
||||
if (!\is_array($request->get('post_note')) || !\array_key_exists('_next', $request->get('post_note'))) {
|
||||
return null;
|
||||
}
|
||||
$next = parse_url($request->get('post_note')['_next']);
|
||||
if (!\array_key_exists('query', $next)) {
|
||||
return null;
|
||||
}
|
||||
parse_str($next['query'], $query);
|
||||
if (!\array_key_exists('reply_to_id', $query)) {
|
||||
return null;
|
||||
}
|
||||
return (int) $query['reply_to_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs **\App\Component\Posting::onAppendRightPostingBlock**, of the **current page context** in which the given
|
||||
* Actor is in. This is valuable when posting within a group route, allowing \App\Component\Posting to create a
|
||||
@@ -195,12 +182,10 @@ class Conversation extends Component
|
||||
*
|
||||
* @param \App\Entity\Actor $actor The Actor currently attempting to post a Note
|
||||
* @param null|\App\Entity\Actor $context_actor The 'owner' of the current route (e.g. Group or Actor), used to target it
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): bool
|
||||
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor)
|
||||
{
|
||||
$to_note_id = $this->getReplyToIdFromRequest($request);
|
||||
$to_note_id = $request->query->get('reply_to_id');
|
||||
if (!\is_null($to_note_id)) {
|
||||
// Getting the actor itself
|
||||
$context_actor = Actor::getById(Note::getById((int) $to_note_id)->getActorId());
|
||||
@@ -209,44 +194,12 @@ class Conversation extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posting event to add extra information to Component\Posting form data
|
||||
*
|
||||
* @param array $data Transport data to be filled with reply_to_id
|
||||
*
|
||||
* @throws \App\Util\Exception\ClientException
|
||||
* @throws \App\Util\Exception\NoSuchNoteException
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onPostingModifyData(Request $request, Actor $actor, array &$data): bool
|
||||
{
|
||||
$to_note_id = $this->getReplyToIdFromRequest($request);
|
||||
if (!\is_null($to_note_id)) {
|
||||
Note::ensureCanInteract(Note::getById($to_note_id), $actor);
|
||||
$data['reply_to_id'] = $to_note_id;
|
||||
}
|
||||
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minimal Note card to RightPanel template
|
||||
*/
|
||||
public function onPrependPostingForm(Request $request, array &$elements): bool
|
||||
{
|
||||
$elements[] = Formatting::twigRenderFile('cards/blocks/note_compact_wrapper.html.twig', ['note' => Note::getById((int) $request->query->get('reply_to_id'))]);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event launched when deleting given Note, it's deletion implies further changes to object related to this Note.
|
||||
* Please note, **replies are NOT deleted**, their reply_to is only set to null since this Note no longer exists.
|
||||
*
|
||||
* @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
|
||||
{
|
||||
@@ -267,7 +220,7 @@ class Conversation extends Component
|
||||
*
|
||||
* @return bool EventHook
|
||||
*/
|
||||
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): bool
|
||||
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions)
|
||||
{
|
||||
if (\is_null($user = Common::user())) {
|
||||
return Event::next;
|
||||
@@ -281,8 +234,7 @@ class Conversation extends Component
|
||||
'conversation_mute',
|
||||
[
|
||||
'conversation_id' => $note->getConversationId(),
|
||||
'from' => $from,
|
||||
'_fragment' => 'note-anchor-' . $note->getId(),
|
||||
'from' => $from . '#note-anchor-' . $note->getId(),
|
||||
],
|
||||
Router::ABSOLUTE_PATH,
|
||||
);
|
||||
@@ -296,14 +248,7 @@ class Conversation extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if ($activity->getObjectType() === 'note' && ConversationMute::isMuted($activity, $actor)) {
|
||||
return Event::stop;
|
||||
|
@@ -26,6 +26,7 @@ namespace Component\Conversation\Entity;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Note;
|
||||
|
||||
/**
|
||||
* Entity class for Conversations
|
||||
|
@@ -37,7 +37,6 @@ namespace Component\Feed\Controller;
|
||||
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Util\Common;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -49,11 +48,9 @@ class Feeds extends FeedController
|
||||
public function public(Request $request): array
|
||||
{
|
||||
$data = $this->query('note-local:true');
|
||||
$page_title = _m(\is_null(Common::user()) ? 'Feed' : 'Planet');
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'page_title' => $page_title,
|
||||
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: $page_title)),
|
||||
'page_title' => _m(\is_null(Common::user()) ? 'Feed' : 'Planet'),
|
||||
'notes' => $data['notes'],
|
||||
];
|
||||
}
|
||||
@@ -68,7 +65,6 @@ class Feeds extends FeedController
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'page_title' => _m('Home'),
|
||||
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: 'Home')),
|
||||
'notes' => $data['notes'],
|
||||
];
|
||||
}
|
||||
|
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Feed\tests\Controller;
|
||||
|
||||
use App\Core\Router\Router;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use Component\Feed\Controller\Feeds;
|
||||
use Jchook\AssertThrows\AssertThrows;
|
||||
|
||||
class FeedsTest extends GNUsocialTestCase
|
||||
{
|
||||
use AssertThrows;
|
||||
|
||||
public function testPublic()
|
||||
{
|
||||
// This calls static::bootKernel(), and creates a "client" that is acting as the browser
|
||||
$client = static::createClient();
|
||||
$crawler = $client->request('GET', Router::url('feed_public'));
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testHome()
|
||||
{
|
||||
// This calls static::bootKernel(), and creates a "client" that is acting as the browser
|
||||
$client = static::createClient();
|
||||
$crawler = $client->request('GET', Router::url('feed_home'));
|
||||
$this->assertResponseStatusCodeSame(302);
|
||||
}
|
||||
|
||||
// TODO: It would be nice to actually test whether the feeds are respecting scopes and spitting
|
||||
// out the expected notes... The ActivityPub plugin have a somewhat obvious way of testing it so,
|
||||
// for now, having that, might fill that need, let's see
|
||||
}
|
@@ -20,7 +20,7 @@ class HostMeta extends XrdController
|
||||
|
||||
public function setXRD()
|
||||
{
|
||||
if (Event::handle('StartHostMetaLinks', [&$this->xrd->links])) {
|
||||
if (Event::handle('StartHostMetaLinks', [&$this->xrd->links]) !== Event::stop) {
|
||||
Event::handle('EndHostMetaLinks', [&$this->xrd->links]);
|
||||
}
|
||||
}
|
||||
|
@@ -125,7 +125,7 @@ class FreeNetworkActorProtocol extends Entity
|
||||
} else {
|
||||
$attributed_protocol->setProtocol($protocol);
|
||||
}
|
||||
DB::persist($attributed_protocol);
|
||||
DB::wrapInTransaction(fn () => DB::persist($attributed_protocol));
|
||||
}
|
||||
|
||||
public static function canIActor(string $protocol, int|Actor $actor_id): bool
|
||||
|
@@ -44,7 +44,6 @@ use App\Util\Exception\NicknameTakenException;
|
||||
use App\Util\Exception\NicknameTooLongException;
|
||||
use App\Util\Exception\NoSuchActorException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\Nickname;
|
||||
use Component\FreeNetwork\Controller\Feeds;
|
||||
use Component\FreeNetwork\Controller\HostMeta;
|
||||
@@ -54,7 +53,6 @@ use Component\FreeNetwork\Util\Discovery;
|
||||
use Component\FreeNetwork\Util\WebfingerResource;
|
||||
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor;
|
||||
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote;
|
||||
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||
use Exception;
|
||||
use const PREG_SET_ORDER;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
@@ -78,13 +76,6 @@ class FreeNetwork extends Component
|
||||
public const OAUTH_ACCESS_TOKEN_REL = 'http://apinamespace.org/oauth/access_token';
|
||||
public const OAUTH_REQUEST_TOKEN_REL = 'http://apinamespace.org/oauth/request_token';
|
||||
public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize';
|
||||
private static array $protocols = [];
|
||||
|
||||
public function onInitializeComponent(): bool
|
||||
{
|
||||
Event::handle('AddFreeNetworkProtocol', [&self::$protocols]);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onAddRoute(RouteLoader $m): bool
|
||||
{
|
||||
@@ -161,7 +152,7 @@ class FreeNetwork extends Component
|
||||
$parts = explode('@', mb_substr(urldecode($resource), 5)); // 5 is strlen of 'acct:'
|
||||
if (\count($parts) === 2) {
|
||||
[$nick, $domain] = $parts;
|
||||
if ($domain !== Common::config('site', 'server')) {
|
||||
if ($domain !== $_ENV['SOCIAL_DOMAIN']) {
|
||||
throw new ServerException(_m('Remote profiles not supported via WebFinger yet.'));
|
||||
}
|
||||
|
||||
@@ -178,7 +169,7 @@ class FreeNetwork extends Component
|
||||
// This means $resource is a valid url
|
||||
$resource_parts = parse_url($resource);
|
||||
// TODO: Use URLMatcher
|
||||
if ($resource_parts['host'] === Common::config('site', 'server')) {
|
||||
if ($resource_parts['host'] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) {
|
||||
$str = $resource_parts['path'];
|
||||
// actor_view_nickname
|
||||
$renick = '/\/@(' . Nickname::DISPLAY_FMT . ')\/?/m';
|
||||
@@ -388,7 +379,7 @@ class FreeNetwork extends Component
|
||||
$actor = null;
|
||||
|
||||
$resource_parts = explode($preMention, $target);
|
||||
if ($resource_parts[1] === Common::config('site', 'server')) {
|
||||
if ($resource_parts[1] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) {
|
||||
$actor = LocalUser::getByPK(['nickname' => $resource_parts[0]])->getActor();
|
||||
} else {
|
||||
Event::handle('FreeNetworkFindMentions', [$target, &$actor]);
|
||||
@@ -417,7 +408,7 @@ class FreeNetwork extends Component
|
||||
// This means $resource is a valid url
|
||||
$resource_parts = parse_url($url);
|
||||
// TODO: Use URLMatcher
|
||||
if ($resource_parts['host'] === Common::config('site', 'server')) {
|
||||
if ($resource_parts['host'] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) {
|
||||
$str = $resource_parts['path'];
|
||||
// actor_view_nickname
|
||||
$renick = '/\/@(' . Nickname::DISPLAY_FMT . ')\/?/m';
|
||||
@@ -498,39 +489,22 @@ class FreeNetwork extends Component
|
||||
|
||||
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
|
||||
{
|
||||
foreach (self::$protocols as $protocol) {
|
||||
$protocol::freeNetworkDistribute($sender, $activity, $targets, $reason);
|
||||
$protocols = [];
|
||||
Event::handle('AddFreeNetworkProtocol', [&$protocols]);
|
||||
$delivered = [];
|
||||
foreach ($protocols as $protocol) {
|
||||
$protocol::freeNetworkDistribute($sender, $activity, $targets, $reason, $delivered);
|
||||
}
|
||||
$failed_targets = array_udiff($targets, $delivered, fn (Actor $a, Actor $b): int => $a->getId() <=> $b->getId());
|
||||
// TODO: Implement failed queues
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function mentionTagToName(string $nickname, string $uri): string
|
||||
public static function mentionToName(string $nickname, string $uri): string
|
||||
{
|
||||
return '@' . $nickname . '@' . parse_url($uri, \PHP_URL_HOST);
|
||||
}
|
||||
|
||||
public static function groupTagToName(string $nickname, string $uri): string
|
||||
{
|
||||
return '!' . $nickname . '@' . parse_url($uri, \PHP_URL_HOST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add fediverse: query expression
|
||||
* // TODO: adding WebFinger would probably be nice
|
||||
*/
|
||||
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool
|
||||
{
|
||||
if (Formatting::startsWith($term, ['fediverse:'])) {
|
||||
foreach (self::$protocols as $protocol) {
|
||||
// 10 is strlen of `fediverse:`
|
||||
if ($protocol::freeNetworkGrabRemote(mb_substr($term, 10))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onPluginVersion(array &$versions): bool
|
||||
{
|
||||
$versions[] = [
|
||||
|
@@ -23,52 +23,155 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Group\Controller;
|
||||
|
||||
use App\Core\ActorLocalRoles;
|
||||
use App\Core\Cache;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\UserRoles;
|
||||
use App\Entity as E;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\DuplicateFoundException;
|
||||
use App\Util\Exception\NicknameEmptyException;
|
||||
use App\Util\Exception\NicknameException;
|
||||
use App\Util\Exception\NicknameInvalidException;
|
||||
use App\Util\Exception\NicknameNotAllowedException;
|
||||
use App\Util\Exception\NicknameTakenException;
|
||||
use App\Util\Exception\NicknameTooLongException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Exception\NoLoggedInUser;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Form\ActorForms;
|
||||
use App\Util\Nickname;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Component\Group\Entity\GroupMember;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
use Component\Subscription\Entity\ActorSubscription;
|
||||
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\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Group extends Controller
|
||||
class Group extends FeedController
|
||||
{
|
||||
/**
|
||||
* View a group feed by its nickname
|
||||
*
|
||||
* @param string $nickname The group's nickname to be shown
|
||||
*
|
||||
* @throws NicknameEmptyException
|
||||
* @throws NicknameNotAllowedException
|
||||
* @throws NicknameTakenException
|
||||
* @throws NicknameTooLongException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function groupViewNickname(Request $request, string $nickname)
|
||||
{
|
||||
Nickname::validate($nickname, which: Nickname::CHECK_LOCAL_GROUP); // throws
|
||||
$group = LocalGroup::getActorByNickname($nickname);
|
||||
$actor = Common::actor();
|
||||
$subscribe_form = null;
|
||||
|
||||
if (!\is_null($group)
|
||||
&& !\is_null($actor)
|
||||
&& \is_null(Cache::get(
|
||||
ActorSubscription::cacheKeys($actor, $group)['subscribed'],
|
||||
fn () => DB::findOneBy('actor_subscription', [
|
||||
'subscriber_id' => $actor->getId(),
|
||||
'subscribed_id' => $group->getId(),
|
||||
], return_null: true),
|
||||
))
|
||||
) {
|
||||
$subscribe_form = Form::create([['subscribe', SubmitType::class, ['label' => _m('Subscribe to this group')]]]);
|
||||
$subscribe_form->handleRequest($request);
|
||||
if ($subscribe_form->isSubmitted() && $subscribe_form->isValid()) {
|
||||
DB::persist(ActorSubscription::create([
|
||||
'subscriber_id' => $actor->getId(),
|
||||
'subscribed_id' => $group->getId(),
|
||||
]));
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($group->getId())['subscribers']);
|
||||
Cache::delete(E\Actor::cacheKeys($actor->getId())['subscribed']);
|
||||
Cache::delete(ActorSubscription::cacheKeys($actor, $group)['subscribed']);
|
||||
}
|
||||
}
|
||||
|
||||
$notes = !\is_null($group) ? DB::dql(
|
||||
<<<'EOF'
|
||||
select n from note n
|
||||
join activity a with n.id = a.object_id
|
||||
join group_inbox gi with a.id = gi.activity_id
|
||||
where a.object_type = 'note' and gi.group_id = :group_id
|
||||
order by a.created desc, a.id desc
|
||||
EOF,
|
||||
['group_id' => $group->getId()],
|
||||
) : [];
|
||||
|
||||
return [
|
||||
'_template' => 'group/view.html.twig',
|
||||
'actor' => $group,
|
||||
'nickname' => $group?->getNickname() ?? $nickname,
|
||||
'notes' => $notes,
|
||||
'subscribe_form' => $subscribe_form?->createView(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Page that allows an actor to create a new group
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function groupCreate(Request $request): array
|
||||
public function groupCreate(Request $request)
|
||||
{
|
||||
if (\is_null($actor = Common::actor())) {
|
||||
throw new RedirectException('security_login');
|
||||
}
|
||||
|
||||
$create_form = self::getGroupCreateForm($request, $actor);
|
||||
$create_form = Form::create([
|
||||
['group_nickname', TextType::class, ['label' => _m('Group nickname')]],
|
||||
['group_create', SubmitType::class, ['label' => _m('Create this group!')]],
|
||||
]);
|
||||
|
||||
$create_form->handleRequest($request);
|
||||
if ($create_form->isSubmitted() && $create_form->isValid()) {
|
||||
$data = $create_form->getData();
|
||||
$nickname = $data['group_nickname'];
|
||||
|
||||
Log::info(
|
||||
_m(
|
||||
'Actor id:{actor_id} nick:{actor_nick} created the group {nickname}',
|
||||
['{actor_id}' => $actor->getId(), 'actor_nick' => $actor->getNickname(), 'nickname' => $nickname],
|
||||
),
|
||||
);
|
||||
|
||||
DB::persist($group = E\Actor::create([
|
||||
'nickname' => $nickname,
|
||||
'type' => E\Actor::GROUP,
|
||||
'is_local' => true,
|
||||
'roles' => UserRoles::BOT,
|
||||
]));
|
||||
DB::persist(LocalGroup::create([
|
||||
'group_id' => $group->getId(),
|
||||
'nickname' => $nickname,
|
||||
]));
|
||||
DB::persist(ActorSubscription::create([
|
||||
'subscriber_id' => $group->getId(),
|
||||
'subscribed_id' => $group->getId(),
|
||||
]));
|
||||
DB::persist(GroupMember::create([
|
||||
'group_id' => $group->getId(),
|
||||
'actor_id' => $actor->getId(),
|
||||
'is_admin' => true,
|
||||
]));
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($actor->getId())['subscribers']);
|
||||
Cache::delete(E\Actor::cacheKeys($actor->getId())['subscribed']);
|
||||
|
||||
throw new RedirectException();
|
||||
}
|
||||
|
||||
return [
|
||||
'_template' => 'group/create.html.twig',
|
||||
@@ -80,104 +183,30 @@ class Group extends Controller
|
||||
* Settings page for the group with the provided nickname, checks if the current actor can administrate given group
|
||||
*
|
||||
* @throws ClientException
|
||||
* @throws DuplicateFoundException
|
||||
* @throws NicknameEmptyException
|
||||
* @throws NicknameException
|
||||
* @throws NicknameInvalidException
|
||||
* @throws NicknameNotAllowedException
|
||||
* @throws NicknameTakenException
|
||||
* @throws NicknameTooLongException
|
||||
* @throws NotFoundException
|
||||
* @throws NoLoggedInUser
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function groupSettings(Request $request, int $id): array
|
||||
public function groupSettings(Request $request, string $nickname)
|
||||
{
|
||||
$local_group = DB::findOneBy(LocalGroup::class, ['actor_id' => $id]);
|
||||
$local_group = LocalGroup::getByNickname($nickname);
|
||||
$group_actor = $local_group->getActor();
|
||||
$actor = Common::actor();
|
||||
if (!\is_null($group_actor) && $actor->canModerate($group_actor)) {
|
||||
if (!\is_null($group_actor) && $actor->canAdmin($group_actor)) {
|
||||
return [
|
||||
'_template' => 'group/settings.html.twig',
|
||||
'group' => $group_actor,
|
||||
'personal_info_form' => ActorForms::personalInfo(request: $request, scope: $actor, target: $group_actor)->createView(),
|
||||
'personal_info_form' => ActorForms::personalInfo($request, $actor, $local_group)->createView(),
|
||||
'open_details_query' => $this->string('open'),
|
||||
];
|
||||
} else {
|
||||
throw new ClientException(_m('You do not have permission to edit settings for the group "{group}"', ['{group}' => $id]), code: 404);
|
||||
throw new ClientException(_m('You do not have permission to edit settings for the group "{group}"', ['{group}' => $nickname]), code: 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Group FormInterface getter
|
||||
*
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public static function getGroupCreateForm(Request $request, E\Actor $actor): FormInterface
|
||||
{
|
||||
$create_form = Form::create([
|
||||
['group_nickname', TextType::class, ['label' => _m('Group nickname')]],
|
||||
['group_type', ChoiceType::class, ['label' => _m('Type:'), 'multiple' => false, 'expanded' => false, 'choices' => [
|
||||
_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!')]],
|
||||
]);
|
||||
|
||||
$create_form->handleRequest($request);
|
||||
if ($create_form->isSubmitted() && $create_form->isValid()) {
|
||||
$data = $create_form->getData();
|
||||
$nickname = Nickname::normalize(
|
||||
nickname: $data['group_nickname'],
|
||||
check_already_used: true,
|
||||
which: Nickname::CHECK_LOCAL_GROUP,
|
||||
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 ' . ($roles & ActorLocalRoles::PRIVATE_GROUP ? 'private' : 'public') . ' group {nickname}',
|
||||
['{actor_id}' => $actor->getId(), 'actor_nick' => $actor->getNickname(), 'nickname' => $nickname],
|
||||
),
|
||||
);
|
||||
|
||||
DB::persist($group = E\Actor::create([
|
||||
'nickname' => $nickname,
|
||||
'type' => E\Actor::GROUP,
|
||||
'is_local' => true,
|
||||
'roles' => $roles,
|
||||
]));
|
||||
DB::persist(LocalGroup::create([
|
||||
'actor_id' => $group->getId(),
|
||||
'type' => $data['group_type'],
|
||||
'nickname' => $nickname,
|
||||
]));
|
||||
DB::persist(ActorSubscription::create([
|
||||
'subscriber_id' => $group->getId(),
|
||||
'subscribed_id' => $group->getId(),
|
||||
]));
|
||||
DB::persist(GroupMember::create([
|
||||
'group_id' => $group->getId(),
|
||||
'actor_id' => $actor->getId(),
|
||||
// Group Owner
|
||||
'roles' => ActorLocalRoles::OPERATOR | ActorLocalRoles::MODERATOR | ActorLocalRoles::PARTICIPANT | ActorLocalRoles::VISITOR,
|
||||
]));
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($actor->getId())['subscribers']);
|
||||
Cache::delete(E\Actor::cacheKeys($actor->getId())['subscribed']);
|
||||
|
||||
throw new RedirectException();
|
||||
}
|
||||
return $create_form;
|
||||
}
|
||||
}
|
||||
|
@@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Group\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity as E;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
use Component\Subscription\Entity\ActorSubscription;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class GroupFeed extends FeedController
|
||||
{
|
||||
/**
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function groupView(Request $request, Actor $group): array
|
||||
{
|
||||
$actor = Common::actor();
|
||||
$subscribe_form = null;
|
||||
|
||||
if (!\is_null($actor)
|
||||
&& \is_null(Cache::get(
|
||||
ActorSubscription::cacheKeys($actor, $group)['subscribed'],
|
||||
fn () => DB::findOneBy('actor_subscription', [
|
||||
'subscriber_id' => $actor->getId(),
|
||||
'subscribed_id' => $group->getId(),
|
||||
], return_null: true),
|
||||
))
|
||||
) {
|
||||
$subscribe_form = Form::create([['subscribe', SubmitType::class, ['label' => _m('Subscribe to this group')]]]);
|
||||
$subscribe_form->handleRequest($request);
|
||||
if ($subscribe_form->isSubmitted() && $subscribe_form->isValid()) {
|
||||
DB::persist(ActorSubscription::create([
|
||||
'subscriber_id' => $actor->getId(),
|
||||
'subscribed_id' => $group->getId(),
|
||||
]));
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($group->getId())['subscribers']);
|
||||
Cache::delete(E\Actor::cacheKeys($actor->getId())['subscribed']);
|
||||
Cache::delete(ActorSubscription::cacheKeys($actor, $group)['subscribed']);
|
||||
}
|
||||
}
|
||||
|
||||
$notes = DB::dql(<<<'EOF'
|
||||
SELECT n FROM \App\Entity\Note AS n
|
||||
WHERE n.id IN (
|
||||
SELECT act.object_id FROM \App\Entity\Activity AS act
|
||||
WHERE act.object_type = 'note' AND act.id IN
|
||||
(SELECT att.activity_id FROM \Component\Notification\Entity\Notification AS att WHERE att.target_id = :id)
|
||||
)
|
||||
EOF, ['id' => $group->getId()]);
|
||||
|
||||
return [
|
||||
'_template' => 'group/view.html.twig',
|
||||
'actor' => $group,
|
||||
'nickname' => $group->getNickname(),
|
||||
'notes' => $notes,
|
||||
'notes_feed_title' => (new Heading(1, [], $group->getNickname() . '\'s feed')),
|
||||
'subscribe_form' => $subscribe_form?->createView(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function groupViewId(Request $request, int $id): array
|
||||
{
|
||||
$group = Actor::getById($id);
|
||||
if (\is_null($group) || !$group->isGroup()) {
|
||||
throw new ClientException(_m('No such group.'), 404);
|
||||
}
|
||||
if ($group->getIsLocal()) {
|
||||
return [
|
||||
'_redirect' => Router::url('group_actor_view_nickname', ['nickname' => $group->getNickname()]),
|
||||
'actor' => $group,
|
||||
];
|
||||
}
|
||||
return $this->groupView($request, $group);
|
||||
}
|
||||
|
||||
/**
|
||||
* View a group feed by its nickname
|
||||
*
|
||||
* @param string $nickname The group's nickname to be shown
|
||||
*
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function groupViewNickname(Request $request, string $nickname): array
|
||||
{
|
||||
$group = LocalGroup::getActorByNickname($nickname);
|
||||
if (\is_null($group)) {
|
||||
throw new ClientException(_m('No such group.'), 404);
|
||||
}
|
||||
return $this->groupView($request, $group);
|
||||
}
|
||||
}
|
100
components/Group/Entity/GroupAlias.php
Normal file
100
components/Group/Entity/GroupAlias.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace Component\Group\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for Group Alias
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet Inc.
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class GroupAlias extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private string $alias;
|
||||
private int $group_id;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
public function setAlias(string $alias): self
|
||||
{
|
||||
$this->alias = mb_substr($alias, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAlias(): string
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
public function setGroupId(int $group_id): self
|
||||
{
|
||||
$this->group_id = $group_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGroupId(): int
|
||||
{
|
||||
return $this->group_id;
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'group_alias',
|
||||
'fields' => [
|
||||
'alias' => ['type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'additional nickname for the group'],
|
||||
'group_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Group.id', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'group id which this is an alias of'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['alias'],
|
||||
'indexes' => [
|
||||
'group_alias_group_id_idx' => ['group_id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
110
components/Group/Entity/GroupBlock.php
Normal file
110
components/Group/Entity/GroupBlock.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace Component\Group\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for Group Block
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet Inc.
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class GroupBlock extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $group_id;
|
||||
private int $blocked_actor;
|
||||
private int $blocker_user;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
public function setGroupId(int $group_id): self
|
||||
{
|
||||
$this->group_id = $group_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGroupId(): int
|
||||
{
|
||||
return $this->group_id;
|
||||
}
|
||||
|
||||
public function setBlockedActor(int $blocked_actor): self
|
||||
{
|
||||
$this->blocked_actor = $blocked_actor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBlockedActor(): int
|
||||
{
|
||||
return $this->blocked_actor;
|
||||
}
|
||||
|
||||
public function setBlockerUser(int $blocker_user): self
|
||||
{
|
||||
$this->blocker_user = $blocker_user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBlockerUser(): int
|
||||
{
|
||||
return $this->blocker_user;
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'group_block',
|
||||
'fields' => [
|
||||
'group_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Group.id', 'multiplicity' => 'many to one', 'not null' => true, 'description' => 'group actor is blocked from'],
|
||||
'blocked_actor' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'actor that is blocked'],
|
||||
'blocker_user' => ['type' => 'int', 'foreign key' => true, 'target' => 'LocalUser.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'user making the block'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['group_id', 'blocked_actor'],
|
||||
];
|
||||
}
|
||||
}
|
103
components/Group/Entity/GroupInbox.php
Normal file
103
components/Group/Entity/GroupInbox.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace Component\Group\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for Group's inbox
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Zach Copley <zach@status.net>
|
||||
* @copyright 2010 StatusNet Inc.
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class GroupInbox extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $group_id;
|
||||
private int $activity_id;
|
||||
private DateTimeInterface $created;
|
||||
|
||||
public function setGroupId(int $group_id): self
|
||||
{
|
||||
$this->group_id = $group_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGroupId(): int
|
||||
{
|
||||
return $this->group_id;
|
||||
}
|
||||
|
||||
public function setActivityId(int $activity_id): self
|
||||
{
|
||||
$this->activity_id = $activity_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActivityId(): int
|
||||
{
|
||||
return $this->activity_id;
|
||||
}
|
||||
|
||||
public function setCreated(DateTimeInterface $created): self
|
||||
{
|
||||
$this->created = $created;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreated(): DateTimeInterface
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'group_inbox',
|
||||
'description' => 'Many-many table listing activities posted to a given group, or which groups a given activity was posted to',
|
||||
'fields' => [
|
||||
'group_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Group.id', 'multiplicity' => 'one to one', 'name' => 'group_inbox_group_id_fkey', 'not null' => true, 'description' => 'group receiving the activity'],
|
||||
'activity_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Activity.id', 'multiplicity' => 'many to one', 'name' => 'group_inbox_activity_id_fkey', 'not null' => true, 'description' => 'activity received'],
|
||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||
],
|
||||
'primary key' => ['group_id', 'activity_id'],
|
||||
'indexes' => [
|
||||
'group_inbox_activity_id_idx' => ['activity_id'],
|
||||
'group_inbox_group_id_created_activity_id_idx' => ['group_id', 'created', 'activity_id'],
|
||||
'group_inbox_created_idx' => ['created'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -21,7 +21,6 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Group\Entity;
|
||||
|
||||
use App\Core\ActorLocalRoles;
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
@@ -45,7 +44,8 @@ class GroupMember extends Entity
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $group_id;
|
||||
private int $actor_id;
|
||||
private int $roles = ActorLocalRoles::VISITOR;
|
||||
private ?bool $is_admin = false;
|
||||
private ?string $uri = null;
|
||||
private DateTimeInterface $created;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
@@ -71,15 +71,26 @@ class GroupMember extends Entity
|
||||
return $this->actor_id;
|
||||
}
|
||||
|
||||
public function setRoles(int $roles): self
|
||||
public function setIsAdmin(?bool $is_admin): self
|
||||
{
|
||||
$this->roles = $roles;
|
||||
$this->is_admin = $is_admin;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRoles(): int
|
||||
public function getIsAdmin(): ?bool
|
||||
{
|
||||
return $this->roles;
|
||||
return $this->is_admin;
|
||||
}
|
||||
|
||||
public function setUri(?string $uri): self
|
||||
{
|
||||
$this->uri = \is_null($uri) ? null : mb_substr($uri, 0, 191);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUri(): ?string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function setCreated(DateTimeInterface $created): self
|
||||
@@ -114,11 +125,15 @@ class GroupMember extends Entity
|
||||
'fields' => [
|
||||
'group_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'group_member_group_id_fkey', 'not null' => true, 'description' => 'foreign key to group table'],
|
||||
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'group_member_actor_id_fkey', 'not null' => true, 'description' => 'foreign key to actor table'],
|
||||
'roles' => ['type' => 'int', 'not null' => true, 'default' => ActorLocalRoles::VISITOR, 'description' => 'Bitmap of permissions this actor has'],
|
||||
'is_admin' => ['type' => 'bool', 'default' => false, 'description' => 'is this actor an admin?'],
|
||||
'uri' => ['type' => 'varchar', 'length' => 191, 'description' => 'universal identifier'],
|
||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['group_id', 'actor_id'],
|
||||
'unique keys' => [
|
||||
'group_member_uri_key' => ['uri'],
|
||||
],
|
||||
'indexes' => [
|
||||
'group_member_actor_id_idx' => ['actor_id'],
|
||||
'group_member_created_idx' => ['created'],
|
||||
|
@@ -21,8 +21,10 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Group\Entity;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Entity;
|
||||
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Exception\NicknameEmptyException;
|
||||
use App\Util\Exception\NicknameException;
|
||||
@@ -51,45 +53,33 @@ class LocalGroup extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $actor_id;
|
||||
private string $nickname;
|
||||
private string $type = 'group';
|
||||
private int $group_id;
|
||||
private ?string $nickname = null;
|
||||
private DateTimeInterface $created;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
public function setActorId(int $actor_id): self
|
||||
public function setGroupId(int $group_id): self
|
||||
{
|
||||
$this->actor_id = $actor_id;
|
||||
$this->group_id = $group_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActorId(): int
|
||||
public function getGroupId(): int
|
||||
{
|
||||
return $this->actor_id;
|
||||
return $this->group_id;
|
||||
}
|
||||
|
||||
public function setNickname(string $nickname): self
|
||||
public function setNickname(?string $nickname): self
|
||||
{
|
||||
$this->nickname = mb_substr($nickname, 0, 64);
|
||||
$this->nickname = \is_null($nickname) ? null : mb_substr($nickname, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNickname(): string
|
||||
public function getNickname(): ?string
|
||||
{
|
||||
return $this->nickname;
|
||||
}
|
||||
|
||||
public function setType(string $type): self
|
||||
{
|
||||
$this->type = mb_substr($type, 0, 64);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setCreated(DateTimeInterface $created): self
|
||||
{
|
||||
$this->created = $created;
|
||||
@@ -117,17 +107,19 @@ class LocalGroup extends Entity
|
||||
|
||||
public function getActor()
|
||||
{
|
||||
return DB::findOneBy(Actor::class, ['id' => $this->actor_id]);
|
||||
return DB::find('actor', ['id' => $this->group_id]);
|
||||
}
|
||||
|
||||
public static function getByNickname(string $nickname): ?self
|
||||
{
|
||||
return DB::findOneBy(self::class, ['nickname' => $nickname]);
|
||||
$res = DB::findBy(self::class, ['nickname' => $nickname]);
|
||||
return $res === [] ? null : $res[0];
|
||||
}
|
||||
|
||||
public static function getActorByNickname(string $nickname): ?Actor
|
||||
{
|
||||
return DB::findOneBy(Actor::class, ['nickname' => $nickname, 'type' => Actor::GROUP]);
|
||||
$res = DB::findBy(Actor::class, ['nickname' => $nickname, 'type' => Actor::GROUP]);
|
||||
return $res === [] ? null : $res[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,13 +151,12 @@ class LocalGroup extends Entity
|
||||
'name' => 'local_group',
|
||||
'description' => 'Record for a user group on the local site, with some additional info not in user_group',
|
||||
'fields' => [
|
||||
'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Group.id', 'multiplicity' => 'one to one', 'name' => 'local_group_group_id_fkey', 'not null' => true, 'description' => 'group represented'],
|
||||
'nickname' => ['type' => 'varchar', 'not null' => true, 'length' => 64, 'description' => 'group represented'],
|
||||
'type' => ['type' => 'varchar', 'not null' => true, 'default' => 'group', 'length' => 64, 'description' => 'Group or Organisation'],
|
||||
'group_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Group.id', 'multiplicity' => 'one to one', 'name' => 'local_group_group_id_fkey', 'not null' => true, 'description' => 'group represented'],
|
||||
'nickname' => ['type' => 'varchar', 'length' => 64, 'description' => 'group represented'],
|
||||
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
|
||||
'modified' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['actor_id'],
|
||||
'primary key' => ['group_id'],
|
||||
'unique keys' => [
|
||||
'local_group_nickname_key' => ['nickname'],
|
||||
],
|
||||
|
@@ -26,56 +26,50 @@ use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\HTML;
|
||||
use App\Util\Nickname;
|
||||
use Component\Circle\Controller\SelfTagsSettings;
|
||||
use Component\Group\Controller as C;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
use Component\Notification\Notification;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Group extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
{
|
||||
$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']);
|
||||
$r->connect(id: 'group_create', uri_path: '/group/new', target: [C\Group::class, 'groupCreate']);
|
||||
$r->connect(id: 'group_actor_settings', uri_path: '/group/{id<\d+>}/settings', target: [C\Group::class, 'groupSettings']);
|
||||
$r->connect(id: 'group_actor_view_nickname', uri_path: '/!{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\Group::class, 'groupViewNickname'], options: ['is_system_path' => false]);
|
||||
$r->connect(id: 'group_settings', uri_path: '/!{nickname<' . Nickname::DISPLAY_FMT . '>}/settings', target: [C\Group::class, 'groupSettings'], options: ['is_system_path' => false]);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a notification for an Actor (such as person or group) which means
|
||||
* it shows up in their home feed and such.
|
||||
*/
|
||||
public function onNewNotificationWithTargets(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): bool
|
||||
{
|
||||
foreach ($targets as $target) {
|
||||
if ($target->isGroup()) {
|
||||
// The Group announces to its subscribers
|
||||
Notification::notify($target, $activity, $target->getSubscribers(), $reason);
|
||||
}
|
||||
}
|
||||
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an <a href=group_actor_settings> to the profile card for groups, if the current actor can access them
|
||||
* Add an <a href=group_settings> to the profile card for groups, if the current actor can access them
|
||||
*/
|
||||
public function onAppendCardProfile(array $vars, array &$res): bool
|
||||
{
|
||||
$actor = Common::actor();
|
||||
$group = $vars['actor'];
|
||||
if (!\is_null($actor) && $group->isGroup()) {
|
||||
if ($actor->canModerate($group)) {
|
||||
$url = Router::url('group_actor_settings', ['id' => $group->getId()]);
|
||||
if (!\is_null($actor) && $group->isGroup() && $actor->canAdmin($group)) {
|
||||
$url = Router::url('group_settings', ['nickname' => $group->getNickname()]);
|
||||
$res[] = HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => _m('Edit group settings'), 'class' => 'profile-extra-actions'], _m('Group settings')]]);
|
||||
}
|
||||
$res[] = HTML::html(['a' => ['attrs' => ['href' => Router::url('blog_post', ['in' => $group->getId()]), 'title' => _m('Make a new blog post'), 'class' => 'profile-extra-actions'], _m('Post in blog')]]);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs)
|
||||
{
|
||||
if ($section === 'profile' && $request->get('_route') === 'group_settings') {
|
||||
$nickname = $request->get('nickname');
|
||||
$group = LocalGroup::getActorByNickname($nickname);
|
||||
$tabs[] = [
|
||||
'title' => 'Self tags',
|
||||
'desc' => 'Add or remove tags on this group',
|
||||
'id' => 'settings-self-tags',
|
||||
'controller' => SelfTagsSettings::settingsSelfTags($request, $group, 'settings-self-tags-details'),
|
||||
];
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
@@ -85,46 +79,27 @@ class Group extends Component
|
||||
*/
|
||||
private function getGroupFromContext(Request $request): ?Actor
|
||||
{
|
||||
if (\is_array($request->get('post_note')) && \array_key_exists('_next', $request->get('post_note'))) {
|
||||
$next = parse_url($request->get('post_note')['_next']);
|
||||
$match = Router::match($next['path']);
|
||||
$route = $match['_route'];
|
||||
$identifier = $match['id'] ?? $match['nickname'] ?? null;
|
||||
} else {
|
||||
$route = $request->get('_route');
|
||||
$identifier = $request->get('id') ?? $request->get('nickname');
|
||||
}
|
||||
if (str_starts_with($route, 'group_actor_view_')) {
|
||||
switch ($route) {
|
||||
case 'group_actor_view_nickname':
|
||||
return LocalGroup::getActorByNickname($identifier);
|
||||
case 'group_actor_view_id':
|
||||
return Actor::getById($identifier);
|
||||
if (str_starts_with($request->get('_route'), 'group_actor_view_')) {
|
||||
if (!\is_null($id = $request->get('id'))) {
|
||||
return Actor::getById((int) $id);
|
||||
} elseif (!\is_null($nickname = $request->get('nickname'))) {
|
||||
return LocalGroup::getActorByNickname($nickname);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): bool
|
||||
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets)
|
||||
{
|
||||
$group = $this->getGroupFromContext($request);
|
||||
if (!\is_null($group)) {
|
||||
$nick = "!{$group->getNickname()}";
|
||||
$nick = '!' . $group->getNickname();
|
||||
$targets[$nick] = $group->getId();
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the context in which Posting's form is to be presented. Passing on $context_actor to Posting's
|
||||
* onAppendRightPostingBlock event, the Group a given $actor is currently browsing.
|
||||
*
|
||||
* Makes it possible to automagically fill in the targets (aka the Group which this $request route is connected to)
|
||||
* in the Posting's form.
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
$ctx = $this->getGroupFromContext($request);
|
||||
if (!\is_null($ctx)) {
|
||||
|
@@ -1,10 +0,0 @@
|
||||
<details class="frame-section section-details-title">
|
||||
<summary class="details-summary-title">
|
||||
<strong>
|
||||
{% trans %}Create a group{% endtrans %}
|
||||
</strong>
|
||||
</summary>
|
||||
<form method="POST" class="section-form">
|
||||
{{ form(create_form) }}
|
||||
</form>
|
||||
</details>
|
@@ -1,10 +1,16 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% import 'cards/macros/settings.html.twig' as macros %}
|
||||
{% import 'settings/macros.html.twig' as macros %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="preload" href="{{ asset('assets/default_theme/css/pages/settings.css') }}" as="style" type="text/css">
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/settings.css') }}">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block body %}
|
||||
<nav class='section-settings'>
|
||||
<h1>Settings</h1>
|
||||
<h2>Settings</h2>
|
||||
<ul>
|
||||
<li>
|
||||
{% set profile_tabs = [{'title': 'Personal Info', 'desc': 'Nickname, Homepage, Bio, Self Tags and more.', 'id': 'settings-personal-info', 'form': personal_info_form}] %}
|
||||
|
@@ -1,16 +1,65 @@
|
||||
{% extends 'collection/notes.html.twig' %}
|
||||
{% extends 'stdgrid.html.twig' %}
|
||||
{% import '/cards/note/view.html.twig' as noteView %}
|
||||
|
||||
{% set nickname = nickname|escape %}
|
||||
{% block title %}{{ nickname }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% if actor is defined and actor is not null %}
|
||||
{% block profile_view %}
|
||||
{% include 'cards/blocks/profile.html.twig' with { 'actor': actor } only %}
|
||||
{% include 'cards/profile/view.html.twig' with { 'actor': actor } only %}
|
||||
{% endblock profile_view %}
|
||||
<hr>
|
||||
|
||||
{% if notes is defined %}
|
||||
{{ parent() }}
|
||||
<article>
|
||||
<header class="feed-header">
|
||||
{% if page_title is defined %}
|
||||
<h1 class="heading-no-margin">{{ page_title | trans }}</h1>
|
||||
{% else %}
|
||||
<h1 class="heading-no-margin">{{ 'Notes' | trans }}</h1>
|
||||
{% endif %}
|
||||
<nav class="feed-actions">
|
||||
<details class="feed-actions-details">
|
||||
<summary>
|
||||
{{ icon('filter', 'icon icon-feed-actions') | raw }} {# button-container #}
|
||||
</summary>
|
||||
<div class="feed-actions-details-dropdown">
|
||||
<menu>
|
||||
{% for block in handle_event('AddFeedActions', app.request, notes is defined and notes is not empty) %}
|
||||
{{ block | raw }}
|
||||
{% endfor %}
|
||||
</menu>
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{% if notes is not empty %}
|
||||
{# Backwards compatibility with hAtom 0.1 #}
|
||||
<section class="feed h-feed hfeed notes" tabindex="0" role="feed">
|
||||
{% for conversation in notes %}
|
||||
{% block current_note %}
|
||||
{% if conversation is instanceof('array') %}
|
||||
{{ noteView.macro_note(conversation['note'], conversation['replies']) }}
|
||||
{% else %}
|
||||
{{ noteView.macro_note(conversation) }}
|
||||
{% endif %}
|
||||
<hr tabindex="0" title="{{ 'End of note and replies.' | trans }}">
|
||||
{% endblock current_note %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="feed h-feed hfeed notes" tabindex="0" role="feed">
|
||||
<strong>{% trans %}No notes yet...{% endtrans %}</strong>
|
||||
</section>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock body %}
|
||||
|
@@ -134,7 +134,7 @@ class Language extends Controller
|
||||
// Stay on same page, but force update and prevent resubmission
|
||||
throw new RedirectException('settings_sort_languages');
|
||||
} else {
|
||||
throw new RedirectException('person_actor_settings', ['id' => $user->getId(), 'open' => 'settings-language-details', '_fragment' => 'settings-language-details']);
|
||||
throw new RedirectException('settings', ['open' => 'account', '_fragment' => 'save_account_info_languages']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -105,25 +105,11 @@ class Language extends Component
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
{
|
||||
$note_aliases = $note_qb->getAllAliases();
|
||||
if (!\in_array('note_language', $note_aliases)) {
|
||||
$note_qb->leftJoin('Component\Language\Entity\Language', 'note_language', Expr\Join::WITH, 'note.language_id = note_language.id');
|
||||
}
|
||||
if (!\in_array('actor_language', $note_aliases)) {
|
||||
$note_qb->leftJoin('Component\Language\Entity\ActorLanguage', 'actor_language', Expr\Join::WITH, 'note.actor_id = actor_language.actor_id');
|
||||
}
|
||||
if (!\in_array('note_actor_language', $note_aliases)) {
|
||||
$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();
|
||||
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');
|
||||
}
|
||||
if (!\in_array('language', $actor_aliases)) {
|
||||
$actor_qb->leftJoin('Component\Language\Entity\Language', 'language', Expr\Join::WITH, 'actor_language.language_id = language.id');
|
||||
}
|
||||
|
||||
$note_qb->leftJoin('Component\Language\Entity\Language', 'note_language', Expr\Join::WITH, 'note.language_id = note_language.id')
|
||||
->leftJoin('Component\Language\Entity\ActorLanguage', 'actor_language', Expr\Join::WITH, 'note.actor_id = actor_language.actor_id')
|
||||
->leftJoin('Component\Language\Entity\Language', 'note_actor_language', Expr\Join::WITH, 'note_actor_language.id = actor_language.language_id');
|
||||
$actor_qb->leftJoin('Component\Language\Entity\ActorLanguage', 'actor_language', Expr\Join::WITH, 'actor.id = actor_language.actor_id')
|
||||
->leftJoin('Component\Language\Entity\Language', 'language', Expr\Join::WITH, 'actor_language.language_id = language.id');
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="frame-section frame-section-padding">
|
||||
<h3>{% trans %}Put the languages in the order you'd like to see them in your language selection dropdown, when posting{% endtrans %}</h3>
|
||||
<h3>{{ 'Put the languages in the order you\'d like to see them in your language selection dropdown, when posting' | trans}}</h3>
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -42,12 +42,7 @@ class LeftPanel extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
$cache_key = Feed::cacheKey($actor);
|
||||
$feeds = Feed::getFeeds($actor);
|
||||
@@ -72,4 +67,17 @@ class LeftPanel extends Component
|
||||
return Event::stop;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$styles[] = 'components/LeftPanel/assets/css/view.css';
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/pages/feeds.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% macro edit_feeds_form_row(child) %}
|
||||
@@ -15,7 +15,7 @@
|
||||
{% block body %}
|
||||
<div class="frame-section">
|
||||
<form class="section-form" action="{{ path('edit_feeds') }}" method="post">
|
||||
<h1 class="frame-section-title">{% trans %}Edit feed navigation links{% endtrans %}</h1>
|
||||
<h1 class="frame-section-title">{{ "Edit feed navigation links" | trans }}</h1>
|
||||
{# Since the form is not separated into individual groups, this happened #}
|
||||
{{ form_start(edit_feeds) }}
|
||||
{{ form_errors(edit_feeds) }}
|
||||
|
@@ -1,24 +1,24 @@
|
||||
{% block leftpanel %}
|
||||
<label class="panel-left-icon" for="toggle-panel-left" tabindex="-1">{{ icon('menu', 'icon icon-left') | raw }}</label>
|
||||
<a id="anchor-left-panel" class="anchor-hidden" tabindex="0" title="{% trans %}Press tab followed by a space to access left panel{% endtrans %}"></a>
|
||||
<input type="checkbox" id="toggle-panel-left" tabindex="0" title="{% trans %}Open left panel{% endtrans %}">
|
||||
<a id="anchor-left-panel" class="anchor-hidden" tabindex="0" title="{{ 'Press tab followed by a space to access left panel' | trans }}"></a>
|
||||
<input type="checkbox" id="toggle-panel-left" tabindex="0" title="{{ 'Open left panel' | trans }}">
|
||||
|
||||
<aside class="section-panel section-panel-left">
|
||||
<section class="panel-content accessibility-target">
|
||||
{% if app.user %}
|
||||
<section class='frame-section frame-section-padding' title="{% trans %}Your profile information{% endtrans %}">
|
||||
{% block profile_view %}{% include 'cards/blocks/profile.html.twig' with { actor: current_actor } %}{% endblock profile_view %}
|
||||
{{ block("profile_current_actor", "cards/blocks/navigation.html.twig") }}
|
||||
<section class='frame-section frame-section-padding' title="{{ 'Your profile information.' | trans }}">
|
||||
{% block profile_view %}{% include 'cards/profile/view.html.twig' with { actor: current_actor } %}{% endblock profile_view %}
|
||||
{{ block("profile_current_actor", "cards/navigation/view.html.twig") }}
|
||||
</section>
|
||||
{% else %}
|
||||
<section>
|
||||
{{ block("profile_security", "cards/blocks/navigation.html.twig") }}
|
||||
{{ block("profile_security", "cards/navigation/view.html.twig") }}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{{ block("feeds", "cards/blocks/navigation.html.twig") }}
|
||||
{{ block("feeds", "cards/navigation/view.html.twig") }}
|
||||
|
||||
{{ block("footer", "cards/blocks/navigation.html.twig") }}
|
||||
{{ block("footer", "cards/navigation/view.html.twig") }}
|
||||
</section>
|
||||
</aside>
|
||||
{% endblock leftpanel %}
|
||||
|
@@ -126,7 +126,7 @@ class Link extends Entity
|
||||
{
|
||||
if (Common::isValidHttpUrl($url)) {
|
||||
// If the URL is a local one, do not create a Link to it
|
||||
if (parse_url($url, \PHP_URL_HOST) === Common::config('site', 'server')) {
|
||||
if (parse_url($url, \PHP_URL_HOST) === $_ENV['SOCIAL_DOMAIN']) {
|
||||
Log::warning("It was attempted to create a Link to a local location {$url}.");
|
||||
// Forbidden
|
||||
throw new InvalidArgumentException(message: "A Link can't point to a local location ({$url}), it must be a remote one", code: 400);
|
||||
|
@@ -38,17 +38,14 @@ class Link extends Component
|
||||
/**
|
||||
* Extract URLs from $content and create the appropriate Link and NoteToLink entities
|
||||
*/
|
||||
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): bool
|
||||
public function onProcessNoteContent(Note $note, string $content): bool
|
||||
{
|
||||
$ignore = $process_note_content_extra_args['ignoreLinks'] ?? [];
|
||||
if (Common::config('attachments', 'process_links')) {
|
||||
$matched_urls = [];
|
||||
preg_match_all($this->getURLRegex(), $content, $matched_urls);
|
||||
// TODO: This solution to ignore mentions when content is in html is far from ideal
|
||||
preg_match_all($this->getURLRegex(), preg_replace('#<a href="(.*?)" class="u-url mention">#', '', $content), $matched_urls);
|
||||
$matched_urls = array_unique($matched_urls[1]);
|
||||
foreach ($matched_urls as $match) {
|
||||
if (\in_array($match, $ignore)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$link_id = Entity\Link::getOrCreate($match)->getId();
|
||||
DB::persist(NoteToLink::create(['link_id' => $link_id, 'note_id' => $note->getId()]));
|
||||
|
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace Component\Notification\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
|
||||
/**
|
||||
* Entity for note attentions
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Diogo Peralta Cordeiro <@diogo.site>
|
||||
* @copyright 2022 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Attention extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $note_id;
|
||||
private int $target_id;
|
||||
|
||||
public function setNoteId(int $note_id): self
|
||||
{
|
||||
$this->note_id = $note_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNoteId(): int
|
||||
{
|
||||
return $this->note_id;
|
||||
}
|
||||
|
||||
public function setTargetId(int $target_id): self
|
||||
{
|
||||
$this->target_id = $target_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTargetId(): int
|
||||
{
|
||||
return $this->target_id;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'note_attention',
|
||||
'description' => 'Note attentions to actors (that are not a mention)',
|
||||
'fields' => [
|
||||
'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'note_id to give attention'],
|
||||
'target_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'actor_id for feed receiver'],
|
||||
],
|
||||
'primary key' => ['note_id', 'target_id'],
|
||||
'indexes' => [
|
||||
'attention_note_id_idx' => ['note_id'],
|
||||
'attention_target_id_idx' => ['target_id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -121,7 +121,7 @@ class Notification extends Entity
|
||||
*/
|
||||
public static function getNotificationTargetIdsByActivity(int|Activity $activity_id): array
|
||||
{
|
||||
$notifications = DB::findBy(self::class, ['activity_id' => \is_int($activity_id) ? $activity_id : $activity_id->getId()]);
|
||||
$notifications = DB::findBy('notification', ['activity_id' => \is_int($activity_id) ? $activity_id : $activity_id->getId()]);
|
||||
$targets = [];
|
||||
foreach ($notifications as $notification) {
|
||||
$targets[] = $notification->getTargetId();
|
||||
@@ -131,16 +131,7 @@ class Notification extends Entity
|
||||
|
||||
public function getNotificationTargetsByActivity(int|Activity $activity_id): array
|
||||
{
|
||||
return DB::findBy(Actor::class, ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]);
|
||||
}
|
||||
|
||||
public static function getAllActivitiesTargetedAtActor(Actor $actor): array
|
||||
{
|
||||
return DB::dql(<<<'EOF'
|
||||
SELECT act FROM \App\Entity\Activity AS act
|
||||
WHERE act.object_type = 'note' AND act.id IN
|
||||
(SELECT att.activity_id FROM \Component\Notification\Entity\Notification AS att WHERE att.target_id = :id)
|
||||
EOF, ['id' => $actor->getId()]);
|
||||
return DB::findBy('actor', ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]);
|
||||
}
|
||||
|
||||
public static function schemaDef(): array
|
||||
|
@@ -26,17 +26,14 @@ use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Queue\Queue;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\FreeNetwork\FreeNetwork;
|
||||
use Component\Group\Entity\GroupInbox;
|
||||
use Component\Notification\Controller\Feed;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class Notification extends Component
|
||||
{
|
||||
@@ -46,10 +43,7 @@ class Notification extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): bool
|
||||
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering)
|
||||
{
|
||||
DB::persist(\App\Entity\Feed::create([
|
||||
'actor_id' => $actor_id,
|
||||
@@ -62,89 +56,58 @@ class Notification extends Component
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a notification for an Actor (such as person or group) which means
|
||||
* Enqueues a notification for an Actor (user or group) which means
|
||||
* it shows up in their home feed and such.
|
||||
*/
|
||||
public function onNewNotification(Actor $sender, Activity $activity, array $ids_already_known = [], ?string $reason = null): bool
|
||||
{
|
||||
$targets = $activity->getNotificationTargets(ids_already_known: $ids_already_known, sender_id: $sender->getId());
|
||||
if (Event::handle('NewNotificationWithTargets', [$sender, $activity, $targets, $reason]) === Event::next) {
|
||||
self::notify($sender, $activity, $targets, $reason);
|
||||
}
|
||||
$this->notify($sender, $activity, $targets, $reason);
|
||||
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onQueueNotificationLocal(Actor $sender, Activity $activity, Actor $target, ?string $reason, array &$retry_args): bool
|
||||
{
|
||||
// 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
|
||||
{
|
||||
if (FreeNetwork::notify($sender, $activity, $targets, $reason)) {
|
||||
return Event::stop;
|
||||
} else {
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring given Activity to Targets's attention
|
||||
*
|
||||
* @return true if successful, false otherwise
|
||||
*/
|
||||
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
|
||||
public function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
|
||||
{
|
||||
$remote_targets = [];
|
||||
foreach ($targets as $target) {
|
||||
if ($target->getIsLocal()) {
|
||||
if ($target->isGroup()) {
|
||||
// FIXME: Make sure we check (for both local and remote) users are in the groups they send to!
|
||||
DB::persist(GroupInbox::create([
|
||||
'group_id' => $target->getId(),
|
||||
'activity_id' => $activity->getId(),
|
||||
]));
|
||||
} else {
|
||||
if ($target->hasBlocked($activity->getActor())) {
|
||||
Log::info("Not saving reply to actor {$target->getId()} from sender {$sender->getId()} because of a block.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (Event::handle('NewNotificationShould', [$activity, $target]) === Event::next) {
|
||||
if ($sender->getId() === $target->getId()
|
||||
|| $activity->getActorId() === $target->getId()) {
|
||||
// The target already knows about this, no need to bother with a notification
|
||||
continue;
|
||||
// TODO: use https://symfony.com/doc/current/notifier.html
|
||||
// XXX: Unideal as in failures the rollback will leave behind a false notification,
|
||||
// but most notifications (all) require flushing the objects first
|
||||
// Should be okay as long as implementors bear this in mind
|
||||
DB::wrapInTransaction(fn() => DB::persist(Entity\Notification::create([
|
||||
'activity_id' => $activity->getId(),
|
||||
'target_id' => $target->getId(),
|
||||
'reason' => $reason,
|
||||
])));
|
||||
}
|
||||
}
|
||||
Queue::enqueue(
|
||||
payload: [$sender, $activity, $target, $reason],
|
||||
queue: 'notification_local',
|
||||
priority: true,
|
||||
);
|
||||
} else {
|
||||
// We have no authority nor responsibility of notifying remote actors of a remote actor's doing
|
||||
if ($sender->getIsLocal()) {
|
||||
$remote_targets[] = $target;
|
||||
}
|
||||
}
|
||||
// XXX: Unideal as in failures the rollback will leave behind a false notification,
|
||||
// but most notifications (all) require flushing the objects first
|
||||
// Should be okay as long as implementors bear this in mind
|
||||
try {
|
||||
DB::wrapInTransaction(fn () => DB::persist(Entity\Notification::create([
|
||||
'activity_id' => $activity->getId(),
|
||||
'target_id' => $target->getId(),
|
||||
'reason' => $reason,
|
||||
])));
|
||||
} catch (Exception|Throwable $e) {
|
||||
// We do our best not to record duplicated notifications, but it's not insane that can happen
|
||||
Log::error('It was attempted to record an invalid notification!', [$e]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($remote_targets !== []) {
|
||||
Queue::enqueue(
|
||||
payload: [$sender, $activity, $remote_targets, $reason],
|
||||
queue: 'notification_remote',
|
||||
priority: false,
|
||||
);
|
||||
}
|
||||
FreeNetwork::notify($sender, $activity, $remote_targets, $reason);
|
||||
|
||||
return true;
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
|
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Person\Controller;
|
||||
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity as E;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class PersonFeed extends FeedController
|
||||
{
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function personViewId(Request $request, int $id): array
|
||||
{
|
||||
$person = Actor::getById($id);
|
||||
if (\is_null($person) || !$person->isPerson()) {
|
||||
throw new ClientException(_m('No such person.'), 404);
|
||||
}
|
||||
if ($person->getIsLocal()) {
|
||||
return [
|
||||
'_redirect' => Router::url('person_actor_view_nickname', ['nickname' => $person->getNickname()]),
|
||||
'actor' => $person,
|
||||
];
|
||||
}
|
||||
return $this->personView($request, $person);
|
||||
}
|
||||
|
||||
/**
|
||||
* View a group feed by its nickname
|
||||
*
|
||||
* @param string $nickname The group's nickname to be shown
|
||||
*
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function personViewNickname(Request $request, string $nickname): array
|
||||
{
|
||||
$user = LocalUser::getByNickname($nickname);
|
||||
if (\is_null($user)) {
|
||||
throw new ClientException(_m('No such person.'), 404);
|
||||
}
|
||||
$person = Actor::getById($user->getId());
|
||||
return $this->personView($request, $person);
|
||||
}
|
||||
|
||||
public function personView(Request $request, Actor $person): array
|
||||
{
|
||||
return [
|
||||
'_template' => 'actor/view.html.twig',
|
||||
'actor' => $person,
|
||||
'nickname' => $person->getNickname(),
|
||||
'notes' => E\Note::getAllNotesByActor($person),
|
||||
'page_title' => _m('{nickname}\'s profile', ['{nickname}' => $person->getNickname()]),
|
||||
'notes_feed_title' => (new Heading(level: 2, classes: ['section-title'], text: 'Notes by ' . $person->getNickname())),
|
||||
];
|
||||
}
|
||||
}
|
@@ -1,165 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// {{{ License
|
||||
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// }}}
|
||||
|
||||
namespace Component\Person\tests\Controller;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Util\GNUsocialTestCase;
|
||||
use Jchook\AssertThrows\AssertThrows;
|
||||
|
||||
class PersonSettingsTest extends GNUsocialTestCase
|
||||
{
|
||||
use AssertThrows;
|
||||
|
||||
/**
|
||||
* @covers \App\Controller\PersonSettings::allSettings
|
||||
* @covers \App\Controller\PersonSettings::personalInfo
|
||||
*/
|
||||
public function testPersonalInfo()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = DB::findOneBy(LocalUser::class, ['nickname' => 'form_personal_info_test_user']);
|
||||
$client->loginUser($user);
|
||||
|
||||
$client->request('GET', Router::url('person_actor_settings', ['id' => $user->getId()]));
|
||||
$this->assertResponseIsSuccessful();
|
||||
$crawler = $client->submitForm('Save personal info', [
|
||||
'save_personal_info[nickname]' => 'form_test_user_new_nickname',
|
||||
'save_personal_info[full_name]' => 'Form User',
|
||||
'save_personal_info[homepage]' => 'https://gnu.org',
|
||||
'save_personal_info[bio]' => 'I was born at a very young age',
|
||||
'save_personal_info[location]' => 'right here',
|
||||
// 'save_personal_info[phone_number]' => '+351908555842', // from fakenumber.net
|
||||
]);
|
||||
$changed_user = DB::findOneBy(LocalUser::class, ['id' => $user->getId()]);
|
||||
$actor = $changed_user->getActor();
|
||||
static::assertSame($changed_user->getNickname(), 'form_test_user_new_nickname');
|
||||
static::assertSame($actor->getNickname(), 'form_test_user_new_nickname');
|
||||
static::assertSame($actor->getFullName(), 'Form User');
|
||||
static::assertSame($actor->getHomepage(), 'https://gnu.org');
|
||||
static::assertSame($actor->getBio(), 'I was born at a very young age');
|
||||
static::assertSame($actor->getLocation(), 'right here');
|
||||
// static::assertSame($changed_user->getPhoneNumber()->getNationalNumber(), '908555842');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \App\Controller\PersonSettings::account
|
||||
* @covers \App\Controller\PersonSettings::allSettings
|
||||
*/
|
||||
public function testEmail()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = DB::findOneBy(LocalUser::class, ['nickname' => 'form_account_test_user']);
|
||||
$client->loginUser($user);
|
||||
|
||||
$client->request('GET', Router::url('person_actor_settings', ['id' => $user->getId()]));
|
||||
$this->assertResponseIsSuccessful();
|
||||
$crawler = $client->submitForm('Save email info', [
|
||||
'save_email[outgoing_email_sanitized]' => 'outgoing@provider.any',
|
||||
'save_email[incoming_email_sanitized]' => 'incoming@provider.any',
|
||||
]);
|
||||
|
||||
$changed_user = DB::findOneBy(LocalUser::class, ['id' => $user->getId()]);
|
||||
static::assertSame($changed_user->getOutgoingEmail(), 'outgoing@provider.any');
|
||||
static::assertSame($changed_user->getIncomingEmail(), 'incoming@provider.any');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \App\Controller\PersonSettings::account
|
||||
* @covers \App\Controller\PersonSettings::allSettings
|
||||
*/
|
||||
public function testCorrectPassword()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$user = DB::findOneBy(LocalUser::class, ['nickname' => 'form_account_test_user']);
|
||||
$client->loginUser($user);
|
||||
|
||||
$client->request('GET', Router::url('person_actor_settings', ['id' => $user->getId()]));
|
||||
$this->assertResponseIsSuccessful();
|
||||
$crawler = $client->submitForm('Save new password', [
|
||||
'save_password[old_password]' => 'foobar',
|
||||
'save_password[password][first]' => 'this is some test password',
|
||||
'save_password[password][second]' => 'this is some test password',
|
||||
]);
|
||||
|
||||
$changed_user = DB::findOneBy(LocalUser::class, ['id' => $user->getId()]);
|
||||
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();
|
||||
$user = DB::findOneBy(LocalUser::class, ['nickname' => 'form_account_test_user']);
|
||||
$client->loginUser($user);
|
||||
|
||||
$client->request('GET', Router::url('person_actor_settings', ['id' => $user->getId()]));
|
||||
$this->assertResponseIsSuccessful();
|
||||
$crawler = $client->submitForm('Save new password', [
|
||||
'save_password[old_password]' => 'some wrong password',
|
||||
'save_password[password][first]' => 'this is some test password',
|
||||
'save_password[password][second]' => 'this is some test password',
|
||||
]);
|
||||
$this->assertResponseStatusCodeSame(500); // 401 in future
|
||||
$this->assertSelectorTextContains('.stacktrace', 'AuthenticationException');
|
||||
}
|
||||
|
||||
// TODO: First actually implement this functionality
|
||||
// /**
|
||||
// * @covers \App\Controller\PersonSettings::allSettings
|
||||
// * @covers \App\Controller\PersonSettings::notifications
|
||||
// */
|
||||
// public function testNotifications()
|
||||
// {
|
||||
// $client = static::createClient();
|
||||
// $user = DB::findOneBy(LocalUser::class, ['nickname' => 'form_account_test_user']);
|
||||
// $client->loginUser($user);
|
||||
//
|
||||
// $client->request('GET', Router::url('person_actor_settings', ['id' => $user->getId()]));
|
||||
// $this->assertResponseIsSuccessful();
|
||||
// $crawler = $client->submitForm('Save notification settings for Email', [
|
||||
// 'save_email[activity_by_subscribed]' => false,
|
||||
// 'save_email[mention]' => true,
|
||||
// 'save_email[reply]' => false,
|
||||
// 'save_email[subscription]' => true,
|
||||
// 'save_email[favorite]' => false,
|
||||
// 'save_email[nudge]' => true,
|
||||
// 'save_email[dm]' => false,
|
||||
// 'save_email[enable_posting]' => true,
|
||||
// ]);
|
||||
// $settings = DB::findOneBy('user_notification_prefs', ['user_id' => $user->getId(), 'transport' => 'email']);
|
||||
// static::assertSame($settings->getActivityBySubscribed(), false);
|
||||
// static::assertSame($settings->getMention(), true);
|
||||
// static::assertSame($settings->getReply(), false);
|
||||
// static::assertSame($settings->getSubscription(), true);
|
||||
// static::assertSame($settings->getFavorite(), false);
|
||||
// static::assertSame($settings->getNudge(), true);
|
||||
// static::assertSame($settings->getDm(), false);
|
||||
// static::assertSame($settings->getEnablePosting(), true);
|
||||
// }
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Component\Posting\Controller;
|
||||
|
||||
use App\Core;
|
||||
use App\Core\Controller;
|
||||
use App\Core\Event;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use Component\Posting\Form;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Posting extends Controller
|
||||
{
|
||||
public function onPost(Request $request): RedirectResponse
|
||||
{
|
||||
$actor = Common::actor();
|
||||
$form = Form\Posting::create($request);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted()) {
|
||||
try {
|
||||
if ($form->isValid()) {
|
||||
$data = $form->getData();
|
||||
Event::handle('PostingModifyData', [$request, $actor, &$data, $form]);
|
||||
|
||||
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.'));
|
||||
}
|
||||
|
||||
if (\is_null(VisibilityScope::tryFrom($data['visibility']))) {
|
||||
throw new ClientException(_m('You have selected an impossible visibility.'));
|
||||
}
|
||||
|
||||
$extra_args = [];
|
||||
Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form]);
|
||||
|
||||
if (\array_key_exists('in', $data) && $data['in'] !== 'public') {
|
||||
$target = $data['in'];
|
||||
}
|
||||
|
||||
\Component\Posting\Posting::storeLocalNote(
|
||||
actor: $actor,
|
||||
content: $data['content'],
|
||||
content_type: $data['content_type'],
|
||||
locale: $data['language'],
|
||||
scope: VisibilityScope::from($data['visibility']),
|
||||
targets: isset($target) ? [$target] : [],
|
||||
reply_to: \array_key_exists('reply_to_id', $data) ? $data['reply_to_id'] : null,
|
||||
attachments: $data['attachments'],
|
||||
process_note_content_extra_args: $extra_args,
|
||||
);
|
||||
|
||||
return Core\Form::forceRedirect($form, $request);
|
||||
}
|
||||
} catch (FormSizeFileException $e) {
|
||||
throw new ClientException(_m('Invalid file size given'), previous: $e);
|
||||
}
|
||||
}
|
||||
throw new ClientException(_m('Invalid form submission'));
|
||||
}
|
||||
}
|
@@ -1,103 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Component\Posting\Form;
|
||||
|
||||
use App\Core\ActorLocalRoles;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Form\FormFields;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
|
||||
class Posting
|
||||
{
|
||||
public static function create(Request $request)
|
||||
{
|
||||
$actor = Common::actor();
|
||||
|
||||
$placeholder_strings = ['How are you feeling?', 'Have something to share?', 'How was your day?'];
|
||||
Event::handle('PostingPlaceHolderString', [&$placeholder_strings]);
|
||||
$placeholder = $placeholder_strings[array_rand($placeholder_strings)];
|
||||
|
||||
$initial_content = '';
|
||||
Event::handle('PostingInitialContent', [&$initial_content]);
|
||||
|
||||
$available_content_types = [
|
||||
_m('Plain Text') => 'text/plain',
|
||||
];
|
||||
Event::handle('PostingAvailableContentTypes', [&$available_content_types]);
|
||||
|
||||
$in_targets = [];
|
||||
Event::handle('PostingFillTargetChoices', [$request, $actor, &$in_targets]);
|
||||
|
||||
$context_actor = null;
|
||||
Event::handle('PostingGetContextActor', [$request, $actor, &$context_actor]);
|
||||
|
||||
$form_params = [];
|
||||
if (!empty($in_targets)) { // @phpstan-ignore-line
|
||||
// Add "none" option to the first of choices
|
||||
$in_targets = array_merge([_m('Public') => 'public'], $in_targets);
|
||||
// Make the context actor the first In target option
|
||||
if (!\is_null($context_actor)) {
|
||||
foreach ($in_targets as $it_nick => $it_id) {
|
||||
if ($it_id === $context_actor->getId()) {
|
||||
unset($in_targets[$it_nick]);
|
||||
$in_targets = array_merge([$it_nick => $it_id], $in_targets);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$form_params[] = ['in', ChoiceType::class, ['label' => _m('In:'), 'multiple' => false, 'expanded' => false, 'choices' => $in_targets]];
|
||||
}
|
||||
|
||||
$visibility_options = [
|
||||
_m('Public') => VisibilityScope::EVERYWHERE->value,
|
||||
_m('Local') => VisibilityScope::LOCAL->value,
|
||||
_m('Addressee') => VisibilityScope::ADDRESSEE->value,
|
||||
];
|
||||
if (!\is_null($context_actor) && $context_actor->isGroup()) { // @phpstan-ignore-line currently an open bug. See https://web.archive.org/web/20220226131651/https://github.com/phpstan/phpstan/issues/6234
|
||||
if ($actor->canModerate($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, '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.')]];
|
||||
$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) {
|
||||
$form_params[] = ['content_type', ChoiceType::class,
|
||||
[
|
||||
'label' => _m('Text format:'), 'multiple' => false, 'expanded' => false,
|
||||
'data' => $available_content_types[array_key_first($available_content_types)],
|
||||
'choices' => $available_content_types,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$form_params[] = ['content_type', HiddenType::class, ['data' => $available_content_types[array_key_first($available_content_types)]]];
|
||||
}
|
||||
|
||||
Event::handle('PostingAddFormEntries', [$request, $actor, &$form_params]);
|
||||
|
||||
$form_params[] = ['post_note', SubmitType::class, ['label' => _m('Post')]];
|
||||
|
||||
return Form::create($form_params, form_options: ['action' => Router::url(\Component\Posting\Posting::route)]);
|
||||
}
|
||||
}
|
@@ -23,13 +23,12 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Component\Posting;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\Form;
|
||||
use App\Core\GSFile;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Core\VisibilityScope;
|
||||
use App\Entity\Activity;
|
||||
@@ -41,94 +40,150 @@ use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\DuplicateFoundException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Form\FormFields;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\HTML;
|
||||
use Component\Attachment\Entity\ActorToAttachment;
|
||||
use Component\Attachment\Entity\AttachmentToNote;
|
||||
use Component\Conversation\Conversation;
|
||||
use Component\Language\Entity\Language;
|
||||
use Component\Notification\Entity\Attention;
|
||||
use Functional as F;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
|
||||
class Posting extends Component
|
||||
{
|
||||
public const route = 'posting_form_action';
|
||||
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
{
|
||||
$r->connect(self::route, '/form/posting', Controller\Posting::class);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML render event handler responsible for adding and handling
|
||||
* the result of adding the note submission form, only if a user is logged in
|
||||
*
|
||||
* @throws BugFoundException
|
||||
* @throws ClientException
|
||||
* @throws DuplicateFoundException
|
||||
* @throws RedirectException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function onAddMainRightPanelBlock(Request $request, array &$res): bool
|
||||
public function onAppendRightPostingBlock(Request $request, array &$res): bool
|
||||
{
|
||||
if (\is_null($user = Common::user()) || preg_match('(feed|conversation|group|view)', $request->get('_route')) === 0) {
|
||||
if (\is_null($user = Common::user())) {
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
$res['post_form'] = Form\Posting::create($request)->createView();
|
||||
$actor = $user->getActor();
|
||||
|
||||
return Event::next;
|
||||
$placeholder_strings = ['How are you feeling?', 'Have something to share?', 'How was your day?'];
|
||||
Event::handle('PostingPlaceHolderString', [&$placeholder_strings]);
|
||||
$placeholder = $placeholder_strings[array_rand($placeholder_strings)];
|
||||
|
||||
$initial_content = '';
|
||||
Event::handle('PostingInitialContent', [&$initial_content]);
|
||||
|
||||
$available_content_types = [
|
||||
_m('Plain Text') => 'text/plain',
|
||||
];
|
||||
Event::handle('PostingAvailableContentTypes', [&$available_content_types]);
|
||||
|
||||
$in_targets = [];
|
||||
Event::handle('PostingFillTargetChoices', [$request, $actor, &$in_targets]);
|
||||
|
||||
$context_actor = null;
|
||||
Event::handle('PostingGetContextActor', [$request, $actor, &$context_actor]);
|
||||
|
||||
$form_params = [];
|
||||
if (!empty($in_targets)) { // @phpstan-ignore-line
|
||||
// Add "none" option to the top of choices
|
||||
$in_targets = array_merge([_m('Public') => 'public'], $in_targets);
|
||||
$form_params[] = ['in', ChoiceType::class, ['label' => _m('In:'), 'multiple' => false, 'expanded' => false, 'choices' => $in_targets]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws DuplicateFoundException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public static function storeLocalPage(
|
||||
Actor $actor,
|
||||
?string $content,
|
||||
string $content_type,
|
||||
?string $locale = null,
|
||||
?VisibilityScope $scope = null,
|
||||
array $targets = [],
|
||||
null|int|Note $reply_to = null,
|
||||
array $attachments = [],
|
||||
array $processed_attachments = [],
|
||||
array $process_note_content_extra_args = [],
|
||||
bool $flush_and_notify = true,
|
||||
?string $rendered = null,
|
||||
string $source = 'web',
|
||||
?string $title = null,
|
||||
): array {
|
||||
[$activity, $note, $attention_ids] = self::storeLocalNote(
|
||||
actor: $actor,
|
||||
content: $content,
|
||||
// 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::EVERYWHERE->value,
|
||||
_m('Local') => VisibilityScope::LOCAL->value,
|
||||
_m('Addressee') => VisibilityScope::ADDRESSEE->value,
|
||||
]]];
|
||||
$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) {
|
||||
$form_params[] = ['content_type', ChoiceType::class,
|
||||
[
|
||||
'label' => _m('Text format:'), 'multiple' => false, 'expanded' => false,
|
||||
'data' => $available_content_types[array_key_first($available_content_types)],
|
||||
'choices' => $available_content_types,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Event::handle('PostingAddFormEntries', [$request, $actor, &$form_params]);
|
||||
|
||||
$form_params[] = ['post_note', SubmitType::class, ['label' => _m('Post')]];
|
||||
$form = Form::create($form_params);
|
||||
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted()) {
|
||||
try {
|
||||
if ($form->isValid()) {
|
||||
$data = $form->getData();
|
||||
Event::handle('PostingModifyData', [$request, $actor, &$data, $form_params, $form]);
|
||||
|
||||
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.'));
|
||||
}
|
||||
|
||||
if (\is_null(VisibilityScope::tryFrom($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)];
|
||||
$extra_args = [];
|
||||
Event::handle('AddExtraArgsToNoteContent', [$request, $actor, $data, &$extra_args, $form_params, $form]);
|
||||
|
||||
if (\array_key_exists('in', $data) && $data['in'] !== 'public') {
|
||||
$target = $data['in'];
|
||||
}
|
||||
|
||||
self::storeLocalNote(
|
||||
actor: $user->getActor(),
|
||||
content: $data['content'],
|
||||
content_type: $content_type,
|
||||
locale: $locale,
|
||||
scope: $scope,
|
||||
targets: $targets,
|
||||
reply_to: $reply_to,
|
||||
attachments: $attachments,
|
||||
processed_attachments: $processed_attachments,
|
||||
process_note_content_extra_args: $process_note_content_extra_args,
|
||||
flush_and_notify: false,
|
||||
rendered: $rendered,
|
||||
source: $source,
|
||||
locale: $data['language'],
|
||||
scope: VisibilityScope::from($data['visibility']),
|
||||
target: $target ?? null, // @phpstan-ignore-line
|
||||
reply_to_id: $data['reply_to_id'],
|
||||
attachments: $data['attachments'],
|
||||
process_note_content_extra_args: $extra_args,
|
||||
);
|
||||
$note->setType('page');
|
||||
$note->setTitle($title);
|
||||
|
||||
if ($flush_and_notify) {
|
||||
// Flush before notification
|
||||
DB::flush();
|
||||
Event::handle('NewNotification', [$actor, $activity, ['object' => $attention_ids], _m('{nickname} created a page {note_id}.', ['{nickname}' => $actor->getNickname(), '{note_id}' => $activity->getObjectId()])]);
|
||||
try {
|
||||
if ($request->query->has('from')) {
|
||||
$from = $request->query->get('from');
|
||||
if (str_contains($from, '#')) {
|
||||
[$from, $fragment] = explode('#', $from);
|
||||
}
|
||||
Router::match($from);
|
||||
throw new RedirectException(url: $from . (isset($fragment) ? '#' . $fragment : ''));
|
||||
}
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
// continue
|
||||
}
|
||||
throw new RedirectException();
|
||||
}
|
||||
} catch (FormSizeFileException $e) {
|
||||
throw new ClientException(_m('Invalid file size given'), previous: $e);
|
||||
}
|
||||
}
|
||||
|
||||
return [$activity, $note, $attention_ids];
|
||||
$res['post_form'] = $form->createView();
|
||||
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,25 +191,14 @@ class Posting extends Component
|
||||
* $actor_id, possibly as a reply to note $reply_to and with flag
|
||||
* $is_local. Sanitizes $content and $attachments
|
||||
*
|
||||
* @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 array $targets 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 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 $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
|
||||
* @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 BugFoundException
|
||||
* @throws ClientException
|
||||
* @throws DuplicateFoundException
|
||||
* @throws ServerException
|
||||
*
|
||||
* @return array [Activity, Note, int[]] Activity, Note, Attention Ids
|
||||
*/
|
||||
public static function storeLocalNote(
|
||||
Actor $actor,
|
||||
@@ -162,17 +206,16 @@ class Posting extends Component
|
||||
string $content_type,
|
||||
?string $locale = null,
|
||||
?VisibilityScope $scope = null,
|
||||
array $targets = [],
|
||||
null|int|Note $reply_to = null,
|
||||
null|Actor|int $target = null,
|
||||
?int $reply_to_id = null,
|
||||
array $attachments = [],
|
||||
array $processed_attachments = [],
|
||||
array $process_note_content_extra_args = [],
|
||||
bool $flush_and_notify = true,
|
||||
bool $notify = true,
|
||||
?string $rendered = null,
|
||||
string $source = 'web',
|
||||
): array {
|
||||
): Note {
|
||||
$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());
|
||||
$mentions = [];
|
||||
if (\is_null($rendered) && !empty($content)) {
|
||||
Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $locale, &$mentions]);
|
||||
@@ -203,17 +246,6 @@ class Posting extends Component
|
||||
}
|
||||
|
||||
DB::persist($note);
|
||||
Conversation::assignLocalConversation($note, $reply_to_id);
|
||||
|
||||
// Update replies cache
|
||||
if (!\is_null($reply_to_id)) {
|
||||
Cache::incr(Note::cacheKeys($reply_to_id)['replies-count']);
|
||||
// Not having them cached doesn't mean replies don't exist, but don't push it to the
|
||||
// list, as that means they need to be refetched, or some would be missed
|
||||
if (Cache::exists(Note::cacheKeys($reply_to_id)['replies'])) {
|
||||
Cache::listPushRight(Note::cacheKeys($reply_to_id)['replies'], $note);
|
||||
}
|
||||
}
|
||||
|
||||
// Need file and note ids for the next step
|
||||
$note->setUrl(Router::url('note_view', ['id' => $note->getId()], Router::ABSOLUTE_URL));
|
||||
@@ -221,18 +253,18 @@ class Posting extends Component
|
||||
Event::handle('ProcessNoteContent', [$note, $content, $content_type, $process_note_content_extra_args]);
|
||||
}
|
||||
|
||||
// These are note attachments now, and not just attachments, ensure these relations are ensured
|
||||
if ($processed_attachments !== []) {
|
||||
foreach ($processed_attachments as [$a, $fname]) {
|
||||
// Most attachments should already be associated with its author, but maybe it didn't make sense
|
||||
//for this attachment, or it's simply a repost of an attachment by a different actor
|
||||
if (DB::count('actor_to_attachment', $args = ['attachment_id' => $a->getId(), 'actor_id' => $actor->getId()]) === 0) {
|
||||
DB::persist(ActorToAttachment::create($args));
|
||||
}
|
||||
DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $note->getId(), 'title' => $fname]));
|
||||
$a->livesIncrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
Conversation::assignLocalConversation($note, $reply_to_id);
|
||||
|
||||
$activity = Activity::create([
|
||||
'actor_id' => $actor->getId(),
|
||||
'verb' => 'create',
|
||||
@@ -242,32 +274,29 @@ class Posting extends Component
|
||||
]);
|
||||
DB::persist($activity);
|
||||
|
||||
$attention_ids = [];
|
||||
foreach ($targets as $target) {
|
||||
$target_id = \is_int($target) ? $target : $target->getId();
|
||||
DB::persist(Attention::create(['note_id' => $note->getId(), 'target_id' => $target_id]));
|
||||
$attention_ids[$target_id] = true;
|
||||
if (!\is_null($target)) {
|
||||
$target = \is_int($target) ? Actor::getById($target) : $target;
|
||||
$mentions[] = [
|
||||
'mentioned' => [$target],
|
||||
'type' => match ($target->getType()) {
|
||||
Actor::PERSON => 'mention',
|
||||
Actor::GROUP => 'group',
|
||||
default => throw new ClientException(_m('Unknown target type give in \'In\' field: {target}', ['{target}' => $target?->getNickname() ?? '<null>'])),
|
||||
},
|
||||
'text' => $target->getNickname(),
|
||||
];
|
||||
}
|
||||
$attention_ids = array_keys($attention_ids);
|
||||
|
||||
if ($flush_and_notify) {
|
||||
$mention_ids = F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId())));
|
||||
|
||||
// Flush before notification
|
||||
DB::flush();
|
||||
Event::handle('NewNotification', [
|
||||
$actor,
|
||||
$activity,
|
||||
[
|
||||
'note-attention' => $attention_ids,
|
||||
'object' => F\unique(F\flat_map($mentions, fn (array $m) => F\map($m['mentioned'] ?? [], fn (Actor $a) => $a->getId()))),
|
||||
],
|
||||
_m('{nickname} created a note {note_id}.', [
|
||||
'{nickname}' => $actor->getNickname(),
|
||||
'{note_id}' => $activity->getObjectId(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if ($notify) {
|
||||
Event::handle('NewNotification', [$actor, $activity, ['object' => $mention_ids], _m('{nickname} created a note {note_id}.', ['{nickname}' => $actor->getNickname(), '{note_id}' => $activity->getObjectId()])]);
|
||||
}
|
||||
|
||||
return [$activity, $note, $attention_ids];
|
||||
return $note;
|
||||
}
|
||||
|
||||
public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = [])
|
||||
|
@@ -19,21 +19,23 @@ declare(strict_types = 1);
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace Component\Person;
|
||||
namespace Component\RightPanel;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Util\Nickname;
|
||||
use Component\Person\Controller as C;
|
||||
|
||||
class Person extends Component
|
||||
class RightPanel extends Component
|
||||
{
|
||||
public function onAddRoute(RouteLoader $r): bool
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$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]);
|
||||
$r->connect(id: 'person_actor_settings', uri_path: '/person/{id<\d+>}/settings', target: [C\PersonSettings::class, 'allSettings']);
|
||||
$styles[] = 'components/RightPanel/assets/css/view.css';
|
||||
return Event::next;
|
||||
}
|
||||
}
|
@@ -1,97 +1,65 @@
|
||||
{% macro posting(form) %}
|
||||
<section class="section-form">
|
||||
{{ form_start(form) }}
|
||||
{{ form_errors(form) }}
|
||||
{% if form.in is defined %}
|
||||
{{ form_row(form.in) }}
|
||||
{% endif %}
|
||||
{{ form_row(form.visibility) }}
|
||||
{{ form_row(form.content_type) }}
|
||||
{{ form_row(form.content) }}
|
||||
{{ form_row(form.attachments) }}
|
||||
{% block rightpanel %}
|
||||
<label class="panel-right-icon" for="toggle-panel-right" tabindex="-1">{{ icon('chevron-left', 'icon icon-right') | raw }}</label>
|
||||
<a id="anchor-right-panel" class="anchor-hidden" tabindex="0" title="{{ 'Press tab followed by a space to access right panel' | trans }}"></a>
|
||||
<input type="checkbox" id="toggle-panel-right" tabindex="0" title="{{ 'Open right panel' | trans }}">
|
||||
|
||||
<details class="section-details-subtitle frame-section">
|
||||
<aside class="section-panel section-panel-right">
|
||||
<section class="panel-content accessibility-target">
|
||||
{% set prepend_right_panel = handle_event('PrependRightPanel', request) %}
|
||||
{% for widget in prepend_right_panel %}
|
||||
{{ widget | raw }}
|
||||
{% endfor %}
|
||||
|
||||
{% set current_path = app.request.get('_route') %}
|
||||
{% set blocks = handle_event('AppendRightPostingBlock', request) %}
|
||||
{% if blocks['post_form'] is defined %}
|
||||
<section class="frame-section" title="{{ 'Create a new note.' | trans }}">
|
||||
<details class="section-details-title" open="open"
|
||||
title="{{ 'Expand if you want to access more options.' | trans }}">
|
||||
<summary class="details-summary-title">
|
||||
<h2>
|
||||
{% set current_path = app.request.get('_route') %}
|
||||
{% if current_path == 'conversation_reply_to' %}
|
||||
{{ "Reply to note" | trans }}
|
||||
{% else %}
|
||||
{{ "Create a note" | trans }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
<section class="section-form">
|
||||
{{ form_start(blocks['post_form']) }}
|
||||
{{ form_errors(blocks['post_form']) }}
|
||||
{% if blocks['post_form'].in is defined %}
|
||||
{{ form_row(blocks['post_form'].in) }}
|
||||
{% endif %}
|
||||
{{ form_row(blocks['post_form'].visibility) }}
|
||||
{{ form_row(blocks['post_form'].content_type) }}
|
||||
{{ form_row(blocks['post_form'].content) }}
|
||||
{{ form_row(blocks['post_form'].attachments) }}
|
||||
|
||||
<details class="section-details-subtitle">
|
||||
<summary class="details-summary-subtitle">
|
||||
<strong>
|
||||
{% trans %}Additional options{% endtrans %}
|
||||
{{ "Additional options" | trans }}
|
||||
</strong>
|
||||
</summary>
|
||||
<section class="section-form">
|
||||
{{ form_row(form.language) }}
|
||||
{{ form_row(form.tag_use_canonical) }}
|
||||
{{ form_row(blocks['post_form'].language) }}
|
||||
{{ form_row(blocks['post_form'].tag_use_canonical) }}
|
||||
</section>
|
||||
</details>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
{{ form_rest(blocks['post_form']) }}
|
||||
{{ form_end(blocks['post_form']) }}
|
||||
</section>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro posting_section_vanilla(widget) %}
|
||||
<section class="frame-section" title="{% trans %}Create a new note{% endtrans %}">
|
||||
<details class="section-details-title" open="open"
|
||||
title="{% trans %}Expand if you want to access more options{% endtrans %}">
|
||||
<summary class="details-summary-title">
|
||||
<span>
|
||||
{% trans %}Create a note{% endtrans %}
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
{% import _self as forms %}
|
||||
{{ forms.posting(widget) }}
|
||||
</details>
|
||||
</section>
|
||||
{% endmacro %}
|
||||
{% endif %}
|
||||
|
||||
{% macro posting_section_reply(widget, extra) %}
|
||||
<section class="frame-section" title="{% trans %}Create a new note{% endtrans %}">
|
||||
<details class="section-details-title" open="open"
|
||||
title="{% trans %}Expand if you want to access more options{% endtrans %}">
|
||||
<summary class="details-summary-title">
|
||||
<span>
|
||||
{% trans %}Reply to note{% endtrans %}
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
{% for block in extra %}
|
||||
<section class="posting-extra">
|
||||
{% set extra_blocks = get_right_panel_blocks({'path': current_path, 'request': app.request, 'vars': (right_panel_vars | default)}) %}
|
||||
{% for block in extra_blocks %}
|
||||
{{ block | raw }}
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
{% import _self as forms %}
|
||||
{{ forms.posting(widget) }}
|
||||
</details>
|
||||
</section>
|
||||
{% endmacro %}
|
||||
|
||||
{% block rightpanel %}
|
||||
{% import _self as this %}
|
||||
<label class="panel-right-icon" for="toggle-panel-right"
|
||||
tabindex="-1">{{ icon('chevron-left', 'icon icon-right') | raw }}</label>
|
||||
<a id="anchor-right-panel" class="anchor-hidden" tabindex="0"
|
||||
title="{% trans %}Press tab followed by a space to access right panel{% endtrans %}"></a>
|
||||
<input type="checkbox" id="toggle-panel-right" tabindex="0" title="{% trans %}Open right panel{% endtrans %}"
|
||||
{% if app.request.get('_route') == 'conversation_reply_to' %}checked{% endif %}>
|
||||
|
||||
<aside class="section-panel section-panel-right">
|
||||
{% set var_list = {'path': app.request.get('_route'), 'request': app.request, 'vars': right_panel_vars | default } %}
|
||||
{% set blocks = add_right_panel_block('prepend', var_list) %}
|
||||
{% set blocks = blocks|merge(add_right_panel_block('main', var_list)) %}
|
||||
{% set blocks = blocks|merge(add_right_panel_block('append', var_list)) %}
|
||||
|
||||
<section class="panel-content accessibility-target">
|
||||
{% for widget in blocks %}
|
||||
{% if widget is iterable and widget.vars.id == 'post_note' %}
|
||||
{% if app.request.get('_route') == 'conversation_reply_to' %}
|
||||
{% set extra = handle_event('PrependPostingForm', request) %}
|
||||
{{ this.posting_section_reply(widget, extra) }}
|
||||
{% else %}
|
||||
{{ this.posting_section_vanilla(widget) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ widget | raw }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
</aside>
|
||||
</aside>
|
||||
{% endblock rightpanel %}
|
||||
|
@@ -30,7 +30,6 @@ use App\Util\Exception\BugFoundException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Form\FormFields;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Component\Search as Comp;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
@@ -136,8 +135,6 @@ class Search extends FeedController
|
||||
'search_form' => Comp\Search::searchForm($request, query: $q, add_subscribe: !\is_null($actor)),
|
||||
'search_builder_form' => $search_builder_form->createView(),
|
||||
'notes' => $notes ?? [],
|
||||
'notes_feed_title' => (new Heading(level: 3, classes: ['section-title'], text: 'Notes found')),
|
||||
'actors_feed_title' => (new Heading(level: 3, classes: ['section-title'], text: 'Actors found')),
|
||||
'actors' => $actors ?? [],
|
||||
'page' => 1, // TODO paginate
|
||||
];
|
||||
|
@@ -28,7 +28,6 @@ use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Formatting;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@@ -64,22 +63,13 @@ class Search extends Component
|
||||
|
||||
if ($add_subscribe) {
|
||||
$form_definition[] = [
|
||||
'title', TextType::class,
|
||||
[
|
||||
'label' => _m('Subscribe to search query'),
|
||||
'help' => _m('By subscribing to a search query, a new feed link will be added to left panel\'s feed navigation menu'),
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'title' => _m('Title for this new feed in your left panel'),
|
||||
'placeholder' => _m('Input desired title...'),
|
||||
],
|
||||
],
|
||||
'title', TextType::class, ['label' => _m('Title'), 'required' => false, 'attr' => ['title' => _m('Title for this new feed in your left panel')]],
|
||||
];
|
||||
$form_definition[] = [
|
||||
'subscribe_to_search',
|
||||
SubmitType::class,
|
||||
[
|
||||
'label' => _m('Subscribe'),
|
||||
'label' => _m('Subscribe to this search'),
|
||||
'attr' => [
|
||||
'title' => _m('Add this search as a feed in your feeds section of the left panel'),
|
||||
],
|
||||
@@ -110,11 +100,8 @@ class Search extends Component
|
||||
/** @var SubmitButton $subscribe */
|
||||
$subscribe = $form->get('subscribe_to_search');
|
||||
if ($subscribe->isClicked()) {
|
||||
if (!\is_null($data['title'])) {
|
||||
// TODO ensure title is set
|
||||
Event::handle('AppendFeed', [$actor, $data['title'], 'search', ['q' => $data['search_query']]]);
|
||||
} else {
|
||||
throw new ClientException(_m('Empty title is not allowed.'));
|
||||
}
|
||||
$redirect = true;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +120,7 @@ class Search extends Component
|
||||
*
|
||||
* @throws RedirectException
|
||||
*/
|
||||
public function onPrependRightPanelBlock(Request $request, array &$elements): bool
|
||||
public function onPrependRightPanel(Request $request, array &$elements)
|
||||
{
|
||||
$elements[] = Formatting::twigRenderFile('cards/search/view.html.twig', ['search' => self::searchForm($request)]);
|
||||
return Event::next;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<section class="section-form form-search" title="{% trans %}Search for notes, actors, and beyond{% endtrans %}">
|
||||
<section class="section-form form-search" title="{{ 'Search for notes, actors, and beyond' | trans }}">
|
||||
{{ form_start(search) }}
|
||||
<span>{{ form_row(search.search_query) }}{{ form_row(search.submit_search) }}</span>
|
||||
{{ form_rest(search) }}
|
||||
|
@@ -1,45 +1,60 @@
|
||||
{% extends 'collection/notes.html.twig' %}
|
||||
|
||||
{% block search_query_simple %}
|
||||
<section>
|
||||
<h1 class="section-title">{% trans %}Search{% endtrans %}</h1>
|
||||
|
||||
{% block body %}
|
||||
{% if error is defined %}
|
||||
<label class="alert alert-danger">
|
||||
{{ error.getMessage() }}
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
<section class="frame-section frame-section-padding">
|
||||
<h2>{% trans %}Search{% endtrans %}</h2>
|
||||
|
||||
{{ form_start(search_form) }}
|
||||
<section class="frame-section section-form">
|
||||
{{ form_errors(search_form) }}
|
||||
{{ form_row(search_form.search_query) }}
|
||||
{% if actor is not null %}
|
||||
<details class="section-details-subtitle frame-section">
|
||||
<details class="section-details-subtitle">
|
||||
<summary class="details-summary-subtitle">
|
||||
<strong>{% trans %}Other options{% endtrans %}</strong>
|
||||
</summary>
|
||||
|
||||
<div class="section-form">
|
||||
<details class="section-details-subtitle">
|
||||
<summary class="details-summary-subtitle">
|
||||
<strong>
|
||||
{% trans %}Extra options{% endtrans %}
|
||||
{% trans %}Save query as a feed{% endtrans %}
|
||||
</strong>
|
||||
</summary>
|
||||
<div class="section-form">
|
||||
{{ form_row(search_form.title) }}
|
||||
{{ form_row(search_form.subscribe_to_search) }}
|
||||
</div>
|
||||
<hr>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{{ form_row(search_form.submit_search) }}
|
||||
{{ form_rest(search_form) }}
|
||||
</section>
|
||||
{{ form_end(search_form)}}
|
||||
</section>
|
||||
{% endblock search_query_simple %}
|
||||
|
||||
{% block search_query_advanced %}
|
||||
{{ form_start(search_builder_form) }}
|
||||
<details class="section-details section-details-title frame-section">
|
||||
<summary class="details-summary-title">
|
||||
<span>{% trans %}Advanced search{% endtrans %}</span>
|
||||
<section class="frame-section">
|
||||
<details class="section-details-subtitle">
|
||||
<summary class="details-summary-subtitle">
|
||||
<strong>{% trans %}Build a search query{% endtrans %}</strong>
|
||||
</summary>
|
||||
|
||||
<section class="frame-section-padding">
|
||||
<details class="section-details-subtitle frame-section">
|
||||
{{ form_start(search_builder_form) }}
|
||||
<div class="section-form">
|
||||
{# actor options, display if first checked, with checkbox trick #}
|
||||
<details class="section-details-subtitle">
|
||||
<summary class="details-summary-subtitle">
|
||||
<strong>{% trans %}People search options{% endtrans %}</strong>
|
||||
</summary>
|
||||
<div class="section-form">
|
||||
<div class="section-checkbox-flex">
|
||||
{{ form_row(search_builder_form.include_actors) }}
|
||||
{{ form_row(search_builder_form.include_actors_people) }}
|
||||
{{ form_row(search_builder_form.include_actors_groups) }}
|
||||
@@ -47,84 +62,58 @@
|
||||
{{ form_row(search_builder_form.include_actors_businesses) }}
|
||||
{{ form_row(search_builder_form.include_actors_organizations) }}
|
||||
{{ form_row(search_builder_form.include_actors_bots) }}
|
||||
</div>
|
||||
<hr>
|
||||
{{ form_row(search_builder_form.actor_tags) }}
|
||||
<hr>
|
||||
{{ form_row(search_builder_form.actor_langs) }}
|
||||
{{ form_row(search_builder_form.actor_tags) }}
|
||||
</div>
|
||||
<hr>
|
||||
</details>
|
||||
|
||||
<details class="section-details-subtitle frame-section">
|
||||
<details class="section-details-subtitle">
|
||||
<summary class="details-summary-subtitle">
|
||||
<strong>{% trans %}Note search options{% endtrans %}</strong>
|
||||
</summary>
|
||||
<div class="section-form">
|
||||
<div class="section-checkbox-flex">
|
||||
{{ form_row(search_builder_form.include_notes) }}
|
||||
{{ form_row(search_builder_form.include_notes_text) }}
|
||||
{{ form_row(search_builder_form.include_notes_media) }}
|
||||
{{ form_row(search_builder_form.include_notes_polls) }}
|
||||
{{ form_row(search_builder_form.include_notes_bookmarks) }}
|
||||
</div>
|
||||
<hr>
|
||||
{{ form_row(search_builder_form.note_tags) }}
|
||||
<hr>
|
||||
{{ form_row(search_builder_form.note_langs) }}
|
||||
<hr>
|
||||
{{ form_row(search_builder_form.note_actor_tags) }}
|
||||
<hr>
|
||||
{{ form_row(search_builder_form.note_tags) }}
|
||||
{{ form_row(search_builder_form.note_actor_langs) }}
|
||||
{{ form_row(search_builder_form.note_actor_tags) }}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{{ form_rest(search_builder_form) }}
|
||||
</section>
|
||||
</details>
|
||||
{{ form_end(search_builder_form) }}
|
||||
{% endblock search_query_advanced %}
|
||||
|
||||
{% block search %}
|
||||
<section class="frame-section frame-section-padding">
|
||||
{% if error is defined %}
|
||||
<label class="alert alert-danger">
|
||||
{{ error.getMessage() }}
|
||||
</label>
|
||||
{% endif %}
|
||||
{{ block('search_query_simple') }}
|
||||
<hr>
|
||||
{{ block('search_query_advanced') }}
|
||||
</details>
|
||||
</div>
|
||||
{{ form_end(search_builder_form) }}
|
||||
</details>
|
||||
</section>
|
||||
</section>
|
||||
{% endblock search %}
|
||||
|
||||
{% block body %}
|
||||
{{ block('search') }}
|
||||
<div class="frame-section frame-section-padding">
|
||||
<h1 class="section-title">{% trans %}Results{% endtrans %}</h1>
|
||||
|
||||
<section>
|
||||
<section class="frame-section frame-section-padding">
|
||||
<h2>{% trans %}Results{% endtrans %}</h2>
|
||||
<div class="frame-section frame-section-padding feed-empty">
|
||||
{% if notes is defined and notes is not empty %}
|
||||
{{ parent() }}
|
||||
{% else %}
|
||||
<h3>{% trans %}No notes found{% endtrans %}</h3>
|
||||
<em>{% trans %}No notes were found for the specified query...{% endtrans %}</em>
|
||||
{% endif %}
|
||||
</section>
|
||||
<hr>
|
||||
<section>
|
||||
</div>
|
||||
|
||||
<div class="frame-section frame-section-padding feed-empty">
|
||||
<h3>{% trans %}Actors found{% endtrans %}</h3>
|
||||
{% if actors is defined and actors is not empty %}
|
||||
{% for actor in actors %}
|
||||
{% include 'cards/blocks/profile.html.twig' with {'actor': actor} %}
|
||||
{% include 'cards/profile/view.html.twig' with {'actor': actor} %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<em>{% trans %}No Actors were found for the specified query...{% endtrans %}</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="frame-section-button-like">
|
||||
{{ "Page: " ~ page }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
|
||||
|
@@ -32,7 +32,7 @@ use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\Collection\Util\ActorControllerTrait;
|
||||
use Component\Collection\Util\Controller\CircleController;
|
||||
use Component\Subscription\Subscription as SubscriptionComponent;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@@ -43,32 +43,30 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
*/
|
||||
class Subscribers extends CircleController
|
||||
{
|
||||
/**
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function subscribersByActor(Request $request, Actor $actor): array
|
||||
use ActorControllerTrait;
|
||||
public function subscribersByActorId(Request $request, int $id)
|
||||
{
|
||||
return [
|
||||
return $this->handleActorById(
|
||||
$id,
|
||||
fn ($actor) => [
|
||||
'actor' => $actor,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function subscribersByActorNickname(Request $request, string $nickname)
|
||||
{
|
||||
return $this->handleActorByNickname(
|
||||
$nickname,
|
||||
fn ($actor) => [
|
||||
'_template' => 'collection/actors.html.twig',
|
||||
'title' => _m('Subscribers'),
|
||||
'empty_message' => _m('No subscribers.'),
|
||||
'sort_form_fields' => [],
|
||||
'page' => $this->int('page') ?? 1,
|
||||
'actors' => $actor->getSubscribers(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function subscribersByActorId(Request $request, int $id): array
|
||||
{
|
||||
$actor = Actor::getById($id);
|
||||
if (\is_null($actor)) {
|
||||
throw new ClientException(_m('No such actor.'), 404);
|
||||
}
|
||||
return $this->subscribersByActor($request, $actor);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -24,9 +24,7 @@ declare(strict_types = 1);
|
||||
namespace Component\Subscription\Controller;
|
||||
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\Collection\Util\ActorControllerTrait;
|
||||
use Component\Collection\Util\Controller\CircleController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
@@ -35,28 +33,29 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
*/
|
||||
class Subscriptions extends CircleController
|
||||
{
|
||||
/**
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
public function subscriptionsByActorId(Request $request, int $id): array
|
||||
use ActorControllerTrait;
|
||||
public function subscriptionsByActorId(Request $request, int $id)
|
||||
{
|
||||
$actor = Actor::getById($id);
|
||||
if (\is_null($actor)) {
|
||||
throw new ClientException(_m('No such actor.'), 404);
|
||||
}
|
||||
return $this->subscriptionsByActor($request, $actor);
|
||||
return $this->handleActorById(
|
||||
$id,
|
||||
fn ($actor) => [
|
||||
'actor' => $actor,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function subscriptionsByActor(Request $request, Actor $actor)
|
||||
public function subscriptionsByActorNickname(Request $request, string $nickname)
|
||||
{
|
||||
return [
|
||||
return $this->handleActorByNickname(
|
||||
$nickname,
|
||||
fn ($actor) => [
|
||||
'_template' => 'collection/actors.html.twig',
|
||||
'title' => _m('Subscriptions'),
|
||||
'empty_message' => _m('Haven\'t subscribed anyone.'),
|
||||
'sort_form_fields' => [],
|
||||
'page' => $this->int('page') ?? 1,
|
||||
'actors' => $actor->getSubscribers(),
|
||||
];
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ use App\Util\Common;
|
||||
use App\Util\Exception\DuplicateFoundException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Nickname;
|
||||
use Component\Subscription\Controller\Subscribers as SubscribersController;
|
||||
use Component\Subscription\Controller\Subscriptions as SubscriptionsController;
|
||||
|
||||
@@ -49,7 +50,9 @@ class Subscription extends Component
|
||||
$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']);
|
||||
$r->connect(id: 'actor_subscriptions_id', uri_path: '/actor/{id<\d+>}/subscriptions', target: [SubscriptionsController::class, 'subscriptionsByActorId']);
|
||||
$r->connect(id: 'actor_subscriptions_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscriptions', target: [SubscriptionsController::class, 'subscriptionsByActorNickname']);
|
||||
$r->connect(id: 'actor_subscribers_id', uri_path: '/actor/{id<\d+>}/subscribers', target: [SubscribersController::class, 'subscribersByActorId']);
|
||||
$r->connect(id: 'actor_subscribers_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscribers', target: [SubscribersController::class, 'subscribersByActorNickname']);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
@@ -67,7 +70,7 @@ class Subscription extends Component
|
||||
$cache_subscriber = Cache::delete(Actor::cacheKeys($subscriber_id)['subscribed']);
|
||||
$cache_subscribed = Cache::delete(Actor::cacheKeys($subscribed_id)['subscribers']);
|
||||
|
||||
return [$cache_subscriber, $cache_subscribed];
|
||||
return [$cache_subscriber,$cache_subscribed];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block body %}
|
||||
{% block profile_view %}
|
||||
{% include 'cards/blocks/profile.html.twig' with { actor: object } %}
|
||||
{% include 'cards/profile/view.html.twig' with { actor: object } %}
|
||||
{% endblock profile_view %}
|
||||
{{ form(form) }}
|
||||
{% endblock body %}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block body %}
|
||||
{% block profile_view %}
|
||||
{% include 'cards/blocks/profile.html.twig' with { actor: object } %}
|
||||
{% include 'cards/profile/view.html.twig' with { actor: object } %}
|
||||
{% endblock profile_view %}
|
||||
{{ form(form) }}
|
||||
{% endblock body %}
|
||||
|
@@ -50,24 +50,13 @@ class NoteTag extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $note_id;
|
||||
private string $tag;
|
||||
private string $canonical;
|
||||
private int $note_id;
|
||||
private bool $use_canonical;
|
||||
private ?int $language_id = null;
|
||||
private DateTimeInterface $created;
|
||||
|
||||
public function setNoteId(int $note_id): self
|
||||
{
|
||||
$this->note_id = $note_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNoteId(): int
|
||||
{
|
||||
return $this->note_id;
|
||||
}
|
||||
|
||||
public function setTag(string $tag): self
|
||||
{
|
||||
$this->tag = mb_substr($tag, 0, 64);
|
||||
@@ -90,6 +79,17 @@ class NoteTag extends Entity
|
||||
return $this->canonical;
|
||||
}
|
||||
|
||||
public function setNoteId(int $note_id): self
|
||||
{
|
||||
$this->note_id = $note_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNoteId(): int
|
||||
{
|
||||
return $this->note_id;
|
||||
}
|
||||
|
||||
public function setUseCanonical(bool $use_canonical): self
|
||||
{
|
||||
$this->use_canonical = $use_canonical;
|
||||
|
@@ -93,7 +93,7 @@ class Tag extends Component
|
||||
'use_canonical' => $extra_args['tag_use_canonical'] ?? false,
|
||||
'language_id' => $lang_id,
|
||||
]));
|
||||
Cache::listPushLeft("tag-{$canonical_tag}", $note);
|
||||
Cache::pushList("tag-{$canonical_tag}", $note);
|
||||
foreach (self::cacheKeys($canonical_tag) as $key) {
|
||||
Cache::delete($key);
|
||||
}
|
||||
@@ -220,12 +220,8 @@ class Tag extends Component
|
||||
|
||||
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool
|
||||
{
|
||||
if (!\in_array('note_tag', $note_qb->getAllAliases())) {
|
||||
$note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id');
|
||||
}
|
||||
if (!\in_array('actor_tag', $actor_qb->getAllAliases())) {
|
||||
$actor_qb->leftJoin(ActorTag::class, 'actor_tag', Expr\Join::WITH, 'actor_tag.tagger = actor.id');
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
|
@@ -2,16 +2,16 @@
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/pages/feeds.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block body %}
|
||||
{% if tag_name is defined and tag_name is not null %}
|
||||
{% if tag_name is instanceof('string') %}
|
||||
<h1>{% trans %}Actors with tag: %tag_name%{% endtrans %}</h1>
|
||||
<h2>{% trans %}Actors with tag: %tag_name%{% endtrans %}</h2>
|
||||
{% else %}
|
||||
{% set tags = tag_name|join(',') %} {# TODO Not ideal, hard to translate #}
|
||||
<h1>{% trans %}Actors with tags: %tags%{% endtrans %}</h1>
|
||||
<h2>{% trans %}Actors with tags: %tags%{% endtrans %}</h2>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
{% endfor %}
|
||||
|
||||
{% for actor in results %}
|
||||
{% block profile_view %}{% include 'cards/blocks/profile.html.twig' %}{% endblock profile_view %}
|
||||
{% block profile_view %}{% include 'cards/profile/view.html.twig' %}{% endblock profile_view %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="frame-section-button-like">
|
||||
<div class="frame-section frame-section-padding">
|
||||
{{ "Page: " ~ page }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,18 +1,18 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% import "/cards/macros/note/factory.html.twig" as NoteFactory %}
|
||||
{% import '/cards/note/view.html.twig' as noteView %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/feeds.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css">
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block body %}
|
||||
{% if tag_name is defined and tag_name is not null %}
|
||||
{% if tag_name is instanceof('string') %}
|
||||
<h1>{% trans %}Notes with tag: %tag_name%{% endtrans %}</h1>
|
||||
<h2>{% trans %}Notes with tag: %tag_name%{% endtrans %}</h2>
|
||||
{% else %}
|
||||
{% set tags = tag_name|join(', ') %} {# TODO Not ideal, hard to translate #}
|
||||
<h1>{% trans %}People with tags: %tags%{% endtrans %}</h1>
|
||||
<h2>{% trans %}People with tags: %tags%{% endtrans %}</h2>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -22,12 +22,11 @@
|
||||
|
||||
{% for note in results %}
|
||||
{% block current_note %}
|
||||
{% set args = { 'type': 'vanilla_full', 'note': note, 'extra': { 'depth': 0 } } %}
|
||||
{{ NoteFactory.constructor(args) }}
|
||||
{{ noteView.macro_note(note) }}
|
||||
{% endblock current_note %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="frame-section-button-like">
|
||||
<div class="frame-section frame-section-padding frame-section-paging">
|
||||
{{ "Page " ~ page }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
134
composer.json
134
composer.json
@@ -19,48 +19,45 @@
|
||||
"lstrojny/functional-php": "^1.17",
|
||||
"masterminds/html5": "^2.7",
|
||||
"mf2/mf2": "^0.4.6",
|
||||
"nyholm/psr7": "^1.4",
|
||||
"odolbeau/phone-number-bundle": "^3.1",
|
||||
"oro/doctrine-extensions": "^2.0",
|
||||
"php-ds/php-ds": "^1.2",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"sensio/framework-extra-bundle": "6.*",
|
||||
"sensio/framework-extra-bundle": "^5.5",
|
||||
"someonewithpc/memcached-polyfill": "^1.0",
|
||||
"someonewithpc/redis-polyfill": "dev-master",
|
||||
"symfony/asset": "^6",
|
||||
"symfony/cache": "^6",
|
||||
"symfony/config": "^6",
|
||||
"symfony/console": "^6",
|
||||
"symfony/doctrine-messenger": "^6",
|
||||
"symfony/dom-crawler": "^6",
|
||||
"symfony/dotenv": "^6",
|
||||
"symfony/event-dispatcher": "^6",
|
||||
"symfony/expression-language": "^6",
|
||||
"symfony/filesystem": "^6",
|
||||
"symfony/asset": "5.4.*",
|
||||
"symfony/cache": "5.4.*",
|
||||
"symfony/config": "5.4.*",
|
||||
"symfony/console": "5.4.*",
|
||||
"symfony/dom-crawler": "5.4.*",
|
||||
"symfony/dotenv": "5.4.*",
|
||||
"symfony/event-dispatcher": "5.4.*",
|
||||
"symfony/expression-language": "5.4.*",
|
||||
"symfony/filesystem": "5.4.*",
|
||||
"symfony/flex": "^1.3.1",
|
||||
"symfony/form": "^6",
|
||||
"symfony/framework-bundle": "^6",
|
||||
"symfony/http-client": "^6",
|
||||
"symfony/intl": "^6",
|
||||
"symfony/mailer": "^6",
|
||||
"symfony/messenger": "^6",
|
||||
"symfony/mime": "^6",
|
||||
"symfony/form": "5.4.*",
|
||||
"symfony/framework-bundle": "5.4.*",
|
||||
"symfony/http-client": "5.4.*",
|
||||
"symfony/intl": "5.4.*",
|
||||
"symfony/mailer": "5.4.*",
|
||||
"symfony/messenger": "5.4.*",
|
||||
"symfony/mime": "5.4.*",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/notifier": "^6",
|
||||
"symfony/process": "^6",
|
||||
"symfony/property-access": "^6",
|
||||
"symfony/property-info": "^6",
|
||||
"symfony/proxy-manager-bridge": "^6",
|
||||
"symfony/psr-http-message-bridge": "^2.1",
|
||||
"symfony/security-bundle": "^6",
|
||||
"symfony/serializer": "^6",
|
||||
"symfony/string": "^6",
|
||||
"symfony/translation": "^6",
|
||||
"symfony/twig-bundle": "^6",
|
||||
"symfony/validator": "^6",
|
||||
"symfony/var-exporter": "^6",
|
||||
"symfony/web-link": "^6",
|
||||
"symfony/yaml": "^6",
|
||||
"symfony/notifier": "5.4.*",
|
||||
"symfony/process": "5.4.*",
|
||||
"symfony/property-access": "5.4.*",
|
||||
"symfony/property-info": "5.4.*",
|
||||
"symfony/proxy-manager-bridge": "5.4.*",
|
||||
"symfony/security-bundle": "5.4.*",
|
||||
"symfony/serializer": "5.4.*",
|
||||
"symfony/string": "5.4.*",
|
||||
"symfony/translation": "5.4.*",
|
||||
"symfony/twig-bundle": "5.4.*",
|
||||
"symfony/validator": "5.4.*",
|
||||
"symfony/var-exporter": "5.4.*",
|
||||
"symfony/web-link": "5.4.*",
|
||||
"symfony/yaml": "5.4.*",
|
||||
"symfonycasts/reset-password-bundle": "^1.9",
|
||||
"symfonycasts/verify-email-bundle": "^1.0",
|
||||
"tgalopin/html-sanitizer-bundle": "^1.2",
|
||||
@@ -71,22 +68,26 @@
|
||||
"wikimedia/composer-merge-plugin": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"codeception/codeception": "^4.1",
|
||||
"codeception/module-phpbrowser": "^2.0",
|
||||
"codeception/module-symfony": "^2.1",
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.4",
|
||||
"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",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/browser-kit": "^6.0",
|
||||
"symfony/css-selector": "^6.0",
|
||||
"symfony/debug-bundle": "^6.0",
|
||||
"symfony/error-handler": "^6.0",
|
||||
"symfony/browser-kit": "^5.4.",
|
||||
"symfony/css-selector": "^5.4.",
|
||||
"symfony/debug-bundle": "^5.4.",
|
||||
"symfony/error-handler": "^5.4.",
|
||||
"symfony/maker-bundle": "^1.14",
|
||||
"symfony/phpunit-bridge": "^6.0",
|
||||
"symfony/stopwatch": "^6.0",
|
||||
"symfony/web-profiler-bundle": "^6.0",
|
||||
"symfony/phpunit-bridge": "^5.4.",
|
||||
"symfony/stopwatch": "5.4.*",
|
||||
"symfony/web-profiler-bundle": "^5.4.",
|
||||
"ulrichsg/getopt-php": "*",
|
||||
"wp-cli/php-cli-tools": "^0.11.13"
|
||||
"wp-cli/php-cli-tools": "^0.11.13",
|
||||
"codeception/module-asserts": "^1.0.0"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": {
|
||||
@@ -107,8 +108,7 @@
|
||||
"App\\": "src/",
|
||||
"Plugin\\": "plugins/",
|
||||
"Component\\": "components/"
|
||||
},
|
||||
"exclude-from-classmap": ["/tests/"]
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
@@ -143,7 +143,7 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "^6"
|
||||
"require": "5.4.*"
|
||||
},
|
||||
"merge-plugin": {
|
||||
"include": [
|
||||
@@ -193,26 +193,40 @@
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "landrok/activitypub",
|
||||
"version": "0.5.7",
|
||||
"require": {
|
||||
"php": "^7.2|^8.0",
|
||||
"guzzlehttp/guzzle": ">=6.3",
|
||||
"monolog/monolog": "^1.12|^2.0",
|
||||
"symfony/http-foundation": ">=3.4",
|
||||
"phpseclib/phpseclib": "^3.0.7",
|
||||
"psr/cache": "*",
|
||||
"symfony/cache": ">=4.0"
|
||||
},
|
||||
"name": "codeception/codeception",
|
||||
"version": "4.1.30",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ActivityPhp\\": "src/ActivityPhp/"
|
||||
}
|
||||
"Codeception\\": "src/Codeception",
|
||||
"Codeception\\Extension\\": "ext"
|
||||
},
|
||||
"files": [
|
||||
"functions.php"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6.0 <9.0",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"codeception/lib-asserts": "^1.0 | 2.0.*@dev",
|
||||
"guzzlehttp/psr7": "^1.4 | ^2.0",
|
||||
"symfony/finder": ">=2.7 <6.0",
|
||||
"symfony/console": ">=2.7 <6.0",
|
||||
"symfony/event-dispatcher": ">=2.7 <6.0",
|
||||
"symfony/yaml": ">=2.7 <6.0",
|
||||
"symfony/css-selector": ">=2.7 <6.0",
|
||||
"behat/gherkin": "^4.4.0",
|
||||
"codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0",
|
||||
"codeception/stub": "^2.0 | ^3.0 | ^4.0"
|
||||
},
|
||||
"bin": [
|
||||
"codecept"
|
||||
],
|
||||
"source": {
|
||||
"url": "https://github.com/landrok/activitypub.git",
|
||||
"url": "https://github.com/someonewithpc/Codeception.git",
|
||||
"type": "git",
|
||||
"reference": "509dd3d"
|
||||
"reference": "4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4443
composer.lock
generated
4443
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ return [
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
|
||||
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
|
@@ -1,5 +0,0 @@
|
||||
when@dev:
|
||||
debug:
|
||||
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
|
||||
# See the "server:dump" command to start a new server.
|
||||
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
|
19
config/packages/dev/monolog.yaml
Normal file
19
config/packages/dev/monolog.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
6
config/packages/dev/web_profiler.yaml
Normal file
6
config/packages/dev/web_profiler.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
@@ -1,8 +1,7 @@
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
csrf_protection: true
|
||||
# http_method_override: false
|
||||
#http_method_override: true
|
||||
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
@@ -10,15 +9,8 @@ framework:
|
||||
handler_id: null
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
storage_factory_id: session.storage.factory.native
|
||||
|
||||
#esi: true
|
||||
#fragments: true
|
||||
php_errors:
|
||||
log: true
|
||||
|
||||
when@test:
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
|
@@ -1,61 +0,0 @@
|
||||
monolog:
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: php://stderr
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
24
config/packages/prod/monolog.yaml
Normal file
24
config/packages/prod/monolog.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
|
||||
deprecation_filter:
|
||||
type: filter
|
||||
handler: deprecation
|
||||
max_level: info
|
||||
channels: ["php"]
|
@@ -20,28 +20,39 @@ security:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
oauth:
|
||||
pattern: ^/oauth
|
||||
api_apps:
|
||||
pattern: ^/api/v1/apps$
|
||||
security: false
|
||||
api_token:
|
||||
pattern: ^/oauth/token$
|
||||
security: false
|
||||
api:
|
||||
provider: local_user
|
||||
pattern: ^/api/
|
||||
security: true
|
||||
stateless: true
|
||||
|
||||
main:
|
||||
lazy: true
|
||||
entry_point: App\Security\Authenticator
|
||||
guard:
|
||||
authenticators:
|
||||
- App\Security\Authenticator
|
||||
provider: local_user
|
||||
form_login:
|
||||
login_path: security_login
|
||||
check_path: security_login
|
||||
enable_csrf: true
|
||||
logout:
|
||||
path: security_logout
|
||||
# where to redirect after logout
|
||||
target: root
|
||||
|
||||
# remember_me:
|
||||
# secret: '%kernel.secret%'
|
||||
# secure: true
|
||||
# httponly: '%remember_me_httponly%'
|
||||
# samesite: '%remember_me_samesite%'
|
||||
# token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider'
|
||||
|
||||
# custom_authenticator: 'App\Core\Security'
|
||||
remember_me:
|
||||
secret: '%kernel.secret%'
|
||||
secure: true
|
||||
httponly: '%remember_me_httponly%'
|
||||
samesite: '%remember_me_samesite%'
|
||||
token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider'
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#firewalls-authentication
|
||||
@@ -52,5 +63,6 @@ security:
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/admin, roles: ROLE_OPERATOR }
|
||||
- { path: ^/settings, roles: ROLE_VISITOR }
|
||||
- { path: ^/admin, roles: ROLE_ADMIN }
|
||||
- { path: ^/settings, roles: ROLE_USER }
|
||||
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED }
|
||||
|
4
config/packages/test/framework.yaml
Normal file
4
config/packages/test/framework.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
framework:
|
||||
test: true
|
||||
session:
|
||||
storage_id: session.storage.mock_file
|
12
config/packages/test/monolog.yaml
Normal file
12
config/packages/test/monolog.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
6
config/packages/test/web_profiler.yaml
Normal file
6
config/packages/test/web_profiler.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
@@ -1,15 +0,0 @@
|
||||
when@dev:
|
||||
web_profiler:
|
||||
toolbar: true
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { only_exceptions: false }
|
||||
|
||||
when@test:
|
||||
web_profiler:
|
||||
toolbar: false
|
||||
intercept_redirects: false
|
||||
|
||||
framework:
|
||||
profiler: { collect: false }
|
@@ -1,4 +0,0 @@
|
||||
when@dev:
|
||||
_errors:
|
||||
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
|
||||
prefix: /_error
|
@@ -1,8 +0,0 @@
|
||||
when@dev:
|
||||
web_profiler_wdt:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
|
||||
prefix: /_wdt
|
||||
|
||||
web_profiler_profiler:
|
||||
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
|
||||
prefix: /_profiler
|
@@ -45,11 +45,11 @@ services:
|
||||
|
||||
Plugin\:
|
||||
resource: '../plugins/*'
|
||||
exclude: '../plugins/*/{scripts,classes,lib,actions,locale,doc,tests}'
|
||||
exclude: '../plugins/*/{scripts,classes,lib,actions,locale,doc}'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
Component\:
|
||||
resource: '../components/*'
|
||||
exclude: '../components/*/{scripts,classes,lib,actions,locale,doc,tests}'
|
||||
exclude: '../components/*/{scripts,classes,lib,actions,locale,doc}'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
|
@@ -32,8 +32,6 @@ if [ ${DB_EXISTS} -ne 0 ]; then
|
||||
php bin/console doctrine:schema:create || exit 1
|
||||
php bin/console app:populate_initial_values || exit 1
|
||||
|
||||
./bin/install_plugins.sh
|
||||
|
||||
echo "GNU social is installed"
|
||||
else
|
||||
echo "GNU social is already installed"
|
||||
|
@@ -10,5 +10,5 @@ if [ "$#" -eq 0 ] || [ -z "$*" ]; then
|
||||
vendor/bin/simple-phpunit -vvv --coverage-html .test_coverage_report
|
||||
else
|
||||
echo "Running with filter"
|
||||
vendor/bin/simple-phpunit -vvv --coverage-html .test_coverage_report $*
|
||||
vendor/bin/simple-phpunit -vvv --coverage-html .test_coverage_report --filter "$*"
|
||||
fi
|
||||
|
@@ -729,6 +729,7 @@ class Validate
|
||||
/**
|
||||
* Substr
|
||||
*
|
||||
* @param string &$date Date
|
||||
* @param string $num Length
|
||||
* @param false|string $opt Unknown
|
||||
*/
|
||||
|
@@ -30,32 +30,10 @@
|
||||
<server name="SHELL_VERBOSITY" value="-1"/>
|
||||
<server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
|
||||
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5"/>
|
||||
<!-- <env name="CACHE_DRIVER" value="array"/> -->
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="MAIL_DRIVER" value="array"/>
|
||||
</php>
|
||||
<testsuites>
|
||||
<testsuite name="Controller">
|
||||
<directory suffix="Test.php">./tests/Controller</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Core">
|
||||
<directory suffix="Test.php">./tests/Core</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Entity">
|
||||
<directory suffix="Test.php">./tests/Entity</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Twig">
|
||||
<directory suffix="Test.php">./tests/Twig</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Util">
|
||||
<directory suffix="Test.php">./tests/Util</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Plugins">
|
||||
<directory suffix="Test.php">./plugins/</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Component">
|
||||
<directory suffix="Test.php">./components/</directory>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<listeners>
|
||||
|
@@ -32,43 +32,41 @@ declare(strict_types = 1);
|
||||
|
||||
namespace Plugin\ActivityPub;
|
||||
|
||||
use ActivityPhp\Type;
|
||||
use ActivityPhp\Type\AbstractObject;
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Event;
|
||||
use App\Core\HTTPClient;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Modules\Plugin;
|
||||
use App\Core\Queue\Queue;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\LocalUser;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\BugFoundException;
|
||||
use App\Util\Exception\NoSuchActorException;
|
||||
use App\Util\Nickname;
|
||||
use Component\Collection\Util\Controller\OrderedCollection;
|
||||
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
|
||||
use Component\FreeNetwork\Util\Discovery;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use const PHP_URL_HOST;
|
||||
use Plugin\ActivityPub\Controller\Inbox;
|
||||
use Plugin\ActivityPub\Controller\Outbox;
|
||||
use Plugin\ActivityPub\Entity\ActivitypubActivity;
|
||||
use Plugin\ActivityPub\Entity\ActivitypubActor;
|
||||
use Plugin\ActivityPub\Entity\ActivitypubObject;
|
||||
use Plugin\ActivityPub\Util\Explorer;
|
||||
use Plugin\ActivityPub\Util\HTTPSignature;
|
||||
use Plugin\ActivityPub\Util\Model;
|
||||
use Plugin\ActivityPub\Util\OrderedCollectionController;
|
||||
use Plugin\ActivityPub\Util\Response\ActivityResponse;
|
||||
use Plugin\ActivityPub\Util\Response\ActorResponse;
|
||||
use Plugin\ActivityPub\Util\Response\NoteResponse;
|
||||
use Plugin\ActivityPub\Util\TypeResponse;
|
||||
use Plugin\ActivityPub\Util\Validator\contentLangModelValidator;
|
||||
use Plugin\ActivityPub\Util\Validator\manuallyApprovesFollowersModelValidator;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use const PREG_SET_ORDER;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
@@ -109,51 +107,6 @@ class ActivityPub extends Plugin
|
||||
return '3.0.0';
|
||||
}
|
||||
|
||||
public static array $activity_streams_two_context = [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
['gs' => 'https://www.gnu.org/software/social/ns#'],
|
||||
['litepub' => 'http://litepub.social/ns#'],
|
||||
['chatMessage' => 'litepub:chatMessage'],
|
||||
[
|
||||
'inConversation' => [
|
||||
'@id' => 'gs:inConversation',
|
||||
'@type' => '@id',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function onInitializePlugin(): bool
|
||||
{
|
||||
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
|
||||
{
|
||||
// TODO: Check if Actor has authority over payload
|
||||
|
||||
// Store Activity
|
||||
$ap_act = Model\Activity::fromJson($type, ['source' => 'ActivityPub']);
|
||||
FreeNetworkActorProtocol::protocolSucceeded(
|
||||
'activitypub',
|
||||
$ap_actor->getActorId(),
|
||||
Discovery::normalize($actor->getNickname() . '@' . parse_url($ap_actor->getInboxUri(), \PHP_URL_HOST)),
|
||||
);
|
||||
$already_known_ids = [];
|
||||
if (!empty($ap_act->_object_mention_ids)) {
|
||||
$already_known_ids = $ap_act->_object_mention_ids;
|
||||
}
|
||||
|
||||
DB::flush();
|
||||
if (Event::handle('ActivityPubNewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]) === Event::next) {
|
||||
Event::handle('NewNotification', [$actor, $ap_act->getActivity(), $already_known_ids, _m('{nickname} attentioned you.', ['{nickname}' => $actor->getNickname()])]);
|
||||
}
|
||||
|
||||
return Event::stop;
|
||||
}
|
||||
|
||||
/**
|
||||
* This code executes when GNU social creates the page routing, and we hook
|
||||
* on this event to add our Inbox and Outbox handler for ActivityPub.
|
||||
@@ -165,7 +118,7 @@ class ActivityPub extends Plugin
|
||||
$r->connect(
|
||||
'activitypub_inbox',
|
||||
'/inbox.json',
|
||||
Inbox::class,
|
||||
[Inbox::class, 'handle'],
|
||||
options: ['format' => self::$accept_headers[0]],
|
||||
);
|
||||
$r->connect(
|
||||
@@ -192,7 +145,7 @@ class ActivityPub extends Plugin
|
||||
// Is remote?
|
||||
!$actor->getIsLocal()
|
||||
// Is in ActivityPub?
|
||||
&& !\is_null($ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))
|
||||
&& !\is_null($ap_actor = ActivitypubActor::getByPK(['actor_id' => $actor->getId()]))
|
||||
// We can only provide a full URL (anything else wouldn't make sense)
|
||||
&& $type === Router::ABSOLUTE_URL
|
||||
) {
|
||||
@@ -210,11 +163,11 @@ class ActivityPub extends Plugin
|
||||
{
|
||||
// Are both in AP?
|
||||
if (
|
||||
!\is_null($ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))
|
||||
&& !\is_null($ap_other = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $other->getId()], return_null: true))
|
||||
!\is_null($ap_actor = ActivitypubActor::getByPK(['actor_id' => $actor->getId()]))
|
||||
&& !\is_null($ap_other = ActivitypubActor::getByPK(['actor_id' => $other->getId()]))
|
||||
) {
|
||||
// Are they both in the same server?
|
||||
$canAdmin = parse_url($ap_actor->getUri(), \PHP_URL_HOST) === parse_url($ap_other->getUri(), \PHP_URL_HOST);
|
||||
$canAdmin = parse_url($ap_actor->getUri(), PHP_URL_HOST) === parse_url($ap_other->getUri(), PHP_URL_HOST);
|
||||
return Event::stop;
|
||||
}
|
||||
|
||||
@@ -233,17 +186,9 @@ class ActivityPub extends Plugin
|
||||
}
|
||||
switch ($route) {
|
||||
case 'actor_view_id':
|
||||
case 'person_actor_view_id':
|
||||
case 'person_actor_view_nickname':
|
||||
case 'group_actor_view_id':
|
||||
case 'group_actor_view_nickname':
|
||||
case 'bot_actor_view_id':
|
||||
case 'bot_actor_view_nickname':
|
||||
case 'actor_view_nickname':
|
||||
$response = ActorResponse::handle($vars['actor']);
|
||||
break;
|
||||
case 'activity_view':
|
||||
$response = ActivityResponse::handle($vars['activity']);
|
||||
break;
|
||||
case 'note_view':
|
||||
$response = NoteResponse::handle($vars['note']);
|
||||
break;
|
||||
@@ -254,8 +199,6 @@ class ActivityPub extends Plugin
|
||||
if (Event::handle('ActivityPubActivityStreamsTwoResponse', [$route, $vars, &$response]) !== Event::stop) {
|
||||
if (is_subclass_of($vars['controller'][0], OrderedCollection::class)) {
|
||||
$response = new TypeResponse(OrderedCollectionController::fromControllerVars($vars)['type']);
|
||||
} else {
|
||||
$response = new JsonResponse(['error' => 'Unknown Object cannot be represented.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,80 +232,6 @@ class ActivityPub extends Plugin
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
/**
|
||||
* The FreeNetwork component will call this function to pull ActivityPub objects by URI
|
||||
*
|
||||
* @param string $uri Query
|
||||
*
|
||||
* @return bool true if imported, false otherwise
|
||||
*/
|
||||
public static function freeNetworkGrabRemote(string $uri): bool
|
||||
{
|
||||
if (Common::isValidHttpUrl($uri)) {
|
||||
try {
|
||||
$object = self::getObjectByUri($uri);
|
||||
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))) {
|
||||
DB::wrapInTransaction(fn () => Model\Actor::fromJson($object));
|
||||
} else {
|
||||
DB::wrapInTransaction(fn () => Model\Activity::fromJson($object));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (Exception|Throwable) {
|
||||
// May be invalid input, we can safely ignore in this case
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function onQueueActivitypubPostman(
|
||||
Actor $sender,
|
||||
Activity $activity,
|
||||
string $inbox,
|
||||
array $to_actors,
|
||||
array &$retry_args,
|
||||
): bool
|
||||
{
|
||||
try {
|
||||
$data = Model::toJson($activity);
|
||||
if ($sender->isGroup()) {
|
||||
// When the sender is a group, we have to wrap it in an Announce activity
|
||||
$data = Type::create('Announce', ['object' => $data])->toJson();
|
||||
}
|
||||
$res = self::postman($sender, $data, $inbox);
|
||||
|
||||
// accumulate errors for later use, if needed
|
||||
$status_code = $res->getStatusCode();
|
||||
if (!($status_code === 200 || $status_code === 202 || $status_code === 409)) {
|
||||
$res_body = json_decode($res->getContent(), true);
|
||||
$retry_args['reason'] ??= [];
|
||||
$retry_args['reason'][] = $res_body['error'] ?? 'An unknown error occurred.';
|
||||
return Event::next;
|
||||
} else {
|
||||
foreach ($to_actors as $actor) {
|
||||
if ($actor->isPerson()) {
|
||||
FreeNetworkActorProtocol::protocolSucceeded(
|
||||
'activitypub',
|
||||
$actor,
|
||||
Discovery::normalize($actor->getNickname() . '@' . parse_url($inbox, \PHP_URL_HOST)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Event::stop;
|
||||
} catch (Exception $e) {
|
||||
Log::error('ActivityPub @ freeNetworkDistribute: ' . $e->getMessage(), [$e]);
|
||||
$retry_args['reason'] ??= [];
|
||||
$retry_args['reason'][] = "Got an exception: {$e->getMessage()}";
|
||||
$retry_args['exception'] ??= [];
|
||||
$retry_args['exception'][] = $e;
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The FreeNetwork component will call this function to distribute this instance's activities
|
||||
*
|
||||
@@ -371,31 +240,55 @@ class ActivityPub extends Plugin
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public static function freeNetworkDistribute(Actor $sender, Activity $activity, array $targets, ?string $reason = null): void
|
||||
public static function freeNetworkDistribute(Actor $sender, Activity $activity, array $targets, ?string $reason = null, array &$delivered = []): bool
|
||||
{
|
||||
$to_addr = [];
|
||||
foreach ($targets as $actor) {
|
||||
if (FreeNetworkActorProtocol::canIActor('activitypub', $actor->getId())) {
|
||||
// Sometimes FreeNetwork can allow us to actor even though we don't have an internal representation of
|
||||
// the actor, that could for example mean that OStatus handled this actor while we were deactivated
|
||||
// On next interaction this should be resolved, for now continue
|
||||
if (\is_null($ap_target = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))) {
|
||||
if (\is_null($ap_target = ActivitypubActor::getByPK(['actor_id' => $actor->getId()]))) {
|
||||
continue;
|
||||
}
|
||||
$to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor;
|
||||
} else {
|
||||
continue;
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($to_addr as $inbox => $to_actors) {
|
||||
Queue::enqueue(
|
||||
payload: [$sender, $activity, $inbox, $to_actors],
|
||||
queue: 'activitypub_postman',
|
||||
priority: false,
|
||||
$errors = [];
|
||||
//$to_failed = [];
|
||||
foreach ($to_addr as $inbox => $dummy) {
|
||||
try {
|
||||
$res = self::postman($sender, Model::toJson($activity), $inbox);
|
||||
|
||||
// accumulate errors for later use, if needed
|
||||
$status_code = $res->getStatusCode();
|
||||
if (!($status_code === 200 || $status_code === 202 || $status_code === 409)) {
|
||||
$res_body = json_decode($res->getContent(), true);
|
||||
$errors[] = $res_body['error'] ?? 'An unknown error occurred.';
|
||||
//$to_failed[$inbox] = $activity;
|
||||
} else {
|
||||
array_push($delivered, ...$dummy);
|
||||
foreach ($dummy as $actor) {
|
||||
FreeNetworkActorProtocol::protocolSucceeded(
|
||||
'activitypub',
|
||||
$actor,
|
||||
Discovery::normalize($actor->getNickname() . '@' . parse_url($inbox, PHP_URL_HOST)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error('ActivityPub @ freeNetworkDistribute: ' . $e->getMessage(), [$e]);
|
||||
//$to_failed[$inbox] = $activity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
Log::error(sizeof($errors) . ' instance/s failed to handle our activity!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal tool to sign and send activities out
|
||||
@@ -470,7 +363,7 @@ class ActivityPub extends Plugin
|
||||
{
|
||||
try {
|
||||
if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) {
|
||||
$ap_actor = DB::wrapInTransaction(fn () => ActivitypubActor::getByAddr($addr));
|
||||
$ap_actor = ActivitypubActor::getByAddr($addr);
|
||||
$actor = Actor::getById($ap_actor->getActorId());
|
||||
FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor->getId(), $addr);
|
||||
return Event::stop;
|
||||
@@ -478,7 +371,7 @@ class ActivityPub extends Plugin
|
||||
return Event::next;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error('ActivityPub WebFinger Mention check failed.', [$e]);
|
||||
Log::error('ActivityPub Webfinger Mention check failed: ' . $e->getMessage());
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
@@ -494,7 +387,7 @@ class ActivityPub extends Plugin
|
||||
return $object->getUrl();
|
||||
} else {
|
||||
// Try known remote objects
|
||||
$known_object = DB::findOneBy(ActivitypubObject::class, ['object_type' => 'note', 'object_id' => $object->getId()], return_null: true);
|
||||
$known_object = ActivitypubObject::getByPK(['object_type' => 'note', 'object_id' => $object->getId()]);
|
||||
if ($known_object instanceof ActivitypubObject) {
|
||||
return $known_object->getObjectUri();
|
||||
} else {
|
||||
@@ -507,8 +400,8 @@ class ActivityPub extends Plugin
|
||||
break;
|
||||
case Activity::class:
|
||||
// Try known remote activities
|
||||
$known_activity = DB::findOneBy(ActivitypubActivity::class, ['activity_id' => $object->getId()], return_null: true);
|
||||
if (!\is_null($known_activity)) {
|
||||
$known_activity = ActivitypubActivity::getByPK(['activity_id' => $object->getId()]);
|
||||
if ($known_activity instanceof ActivitypubActivity) {
|
||||
return $known_activity->getActivityUri();
|
||||
} else {
|
||||
return Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL);
|
||||
@@ -528,29 +421,30 @@ class ActivityPub extends Plugin
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws TransportExceptionInterface
|
||||
*
|
||||
* @return null|Actor|mixed|Note got from URI
|
||||
* @return null|mixed|Note got from URI
|
||||
*/
|
||||
public static function getObjectByUri(string $resource, bool $try_online = true)
|
||||
{
|
||||
// Try known object
|
||||
$known_object = DB::findOneBy(ActivitypubObject::class, ['object_uri' => $resource], return_null: true);
|
||||
if (!\is_null($known_object)) {
|
||||
$known_object = ActivitypubObject::getByPK(['object_uri' => $resource]);
|
||||
if ($known_object instanceof ActivitypubObject) {
|
||||
return $known_object->getObject();
|
||||
}
|
||||
|
||||
// Try known activity
|
||||
$known_activity = DB::findOneBy(ActivitypubActivity::class, ['activity_uri' => $resource], return_null: true);
|
||||
if (!\is_null($known_activity)) {
|
||||
$known_activity = ActivitypubActivity::getByPK(['activity_uri' => $resource]);
|
||||
if ($known_activity instanceof ActivitypubActivity) {
|
||||
return $known_activity->getActivity();
|
||||
}
|
||||
|
||||
// Try local Note
|
||||
if (Common::isValidHttpUrl($resource)) {
|
||||
// This means $resource is a valid url
|
||||
$resource_parts = parse_url($resource);
|
||||
// TODO: Use URLMatcher
|
||||
if ($resource_parts['host'] === Common::config('site', 'server')) {
|
||||
if ($resource_parts['host'] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) {
|
||||
$local_note = DB::findOneBy('note', ['url' => $resource], return_null: true);
|
||||
if (!\is_null($local_note)) {
|
||||
if ($local_note instanceof Note) {
|
||||
return $local_note;
|
||||
}
|
||||
}
|
||||
@@ -558,8 +452,8 @@ class ActivityPub extends Plugin
|
||||
|
||||
// Try Actor
|
||||
try {
|
||||
return Explorer::getOneFromUri($resource, try_online: false);
|
||||
} catch (\Exception) {
|
||||
return self::getActorByUri($resource, try_online: false);
|
||||
} catch (Exception) {
|
||||
// Ignore, this is brute forcing, it's okay not to find
|
||||
}
|
||||
|
||||
@@ -579,4 +473,49 @@ class ActivityPub extends Plugin
|
||||
return Model::jsonToType($response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an Actor from ActivityPub URI, if it doesn't exist, attempt to fetch it
|
||||
* This should only be necessary internally.
|
||||
*
|
||||
* @throws NoSuchActorException
|
||||
*
|
||||
* @return Actor got from URI
|
||||
*/
|
||||
public static function getActorByUri(string $resource, bool $try_online = true): Actor
|
||||
{
|
||||
// Try local
|
||||
if (Common::isValidHttpUrl($resource)) {
|
||||
// This means $resource is a valid url
|
||||
$resource_parts = parse_url($resource);
|
||||
// TODO: Use URLMatcher
|
||||
if ($resource_parts['host'] === $_ENV['SOCIAL_DOMAIN']) { // XXX: 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) {
|
||||
return LocalUser::getByPK(['nickname' => $matches[0][1]])->getActor();
|
||||
} elseif (preg_match_all($reuri, $str, $matches, PREG_SET_ORDER, 0) === 1) {
|
||||
return Actor::getById((int) $matches[0][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try known remote
|
||||
$aprofile = DB::findOneBy(ActivitypubActor::class, ['uri' => $resource], return_null: true);
|
||||
if (!\is_null($aprofile)) {
|
||||
return Actor::getById($aprofile->getActorId());
|
||||
}
|
||||
|
||||
// Try remote
|
||||
if ($try_online) {
|
||||
$aprofile = ActivitypubActor::getByAddr($resource);
|
||||
if ($aprofile instanceof ActivitypubActor) {
|
||||
return Actor::getById($aprofile->getActorId());
|
||||
}
|
||||
}
|
||||
throw new NoSuchActorException("From URI: {$resource}");
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user