- {% if this.googleEnabled %}
-
- Google
+{%- set HAS_ANY_SOCIAL = this.googleEnabled or this.facebookEnabled or this.githubEnabled or this.keycloakEnabled or this.zitadelEnabled or this.azureEnabled -%}
+{% if HAS_ANY_SOCIAL %}
+ {% if not mbin_sso_only_mode() and not mbin_sso_show_first() %}
+
{% endif %}
- {% if this.facebookEnabled %}
-
- Facebook
+
\ No newline at end of file
+
diff --git a/templates/resend_verification_email/resend.html.twig b/templates/resend_verification_email/resend.html.twig
index fb488f3a8..6f69aef31 100644
--- a/templates/resend_verification_email/resend.html.twig
+++ b/templates/resend_verification_email/resend.html.twig
@@ -16,9 +16,9 @@
{% endif %}
diff --git a/templates/layout/_sidebar.html.twig b/templates/layout/_sidebar.html.twig
index 2aa709f8f..0c4401e6f 100644
--- a/templates/layout/_sidebar.html.twig
+++ b/templates/layout/_sidebar.html.twig
@@ -88,6 +88,9 @@
}) }}
{% include 'magazine/_moderators_sidebar.html.twig' %}
{% endif %}
+{% if tag is defined and tag %}
+ {% include 'tag/_panel.html.twig' %}
+{% endif %}
{{ component('related_magazines', {magazine: magazine is defined and magazine ? magazine.name : null, tag: tag is defined and tag ? tag : null}) }}
{% if not is_route_name_contains('people') %}
{{ component('active_users', {magazine: magazine is defined and magazine ? magazine : null}) }}
diff --git a/templates/magazine/panel/tags.html.twig b/templates/magazine/panel/tags.html.twig
index 21a68bfd5..b75b8dfdd 100644
--- a/templates/magazine/panel/tags.html.twig
+++ b/templates/magazine/panel/tags.html.twig
@@ -23,7 +23,6 @@
+ {% endif %}
{% endblock %}
{% block body %}
@@ -19,6 +37,6 @@
'show-comment-avatar': app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_COMMENTS_SHOW_USER_AVATAR')) is same as 'true' or not app.request.cookies.has(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_COMMENTS_SHOW_USER_AVATAR')),
'show-post-avatar': app.request.cookies.get(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_POSTS_SHOW_USERS_AVATARS')) is same as 'true' or not app.request.cookies.has(constant('App\\Controller\\User\\ThemeSettingsController::KBIN_POSTS_SHOW_USERS_AVATARS'))
}) }}">
- {% include 'layout/_subject_list.html.twig' with {entryCommentAttributes: {showMagazineName: true, showEntryTitle: true}, postCommentAttributes: {withPost: false}} %}
+ {% include 'tag/_list.html.twig' %}
{% endblock %}
diff --git a/tests/Unit/Service/TagManagerTest.php b/tests/Unit/Service/TagExtractorTest.php
similarity index 91%
rename from tests/Unit/Service/TagManagerTest.php
rename to tests/Unit/Service/TagExtractorTest.php
index 0a14a3ada..6716b7fa0 100644
--- a/tests/Unit/Service/TagManagerTest.php
+++ b/tests/Unit/Service/TagExtractorTest.php
@@ -4,17 +4,17 @@
namespace App\Tests\Unit\Service;
-use App\Service\TagManager;
+use App\Service\TagExtractor;
use PHPUnit\Framework\TestCase;
-class TagManagerTest extends TestCase
+class TagExtractorTest extends TestCase
{
/**
* @dataProvider provider
*/
public function testExtract(string $input, ?array $output): void
{
- $this->assertEquals($output, (new TagManager())->extract($input, 'kbin'));
+ $this->assertEquals($output, (new TagExtractor())->extract($input, 'kbin'));
}
public static function provider(): array
diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml
index 8d6185de8..b78fb8cd2 100644
--- a/translations/messages.en.yaml
+++ b/translations/messages.en.yaml
@@ -125,6 +125,7 @@ url: URL
title: Title
body: Body
tags: Tags
+tag: Tag
badges: Badges
is_adult: 18+ / NSFW
eng: ENG
@@ -310,6 +311,13 @@ magazine_panel: Magazine panel
reject: Reject
approve: Approve
ban: Ban
+unban: Unban
+ban_hashtag_btn: Ban Hashtag
+ban_hashtag_description: Banning a hashtag will stop posts with this hashtag from being created,
+ as well as hiding existing posts with this hashtag.
+unban_hashtag_btn: Unban Hashtag
+unban_hashtag_description: Unbanning a hashtag will allow creating posts with this hashtag again.
+ Existing posts with this hashtag are no longer hidden.
filters: Filters
approved: Approved
rejected: Rejected
@@ -732,6 +740,7 @@ position_bottom: Bottom
position_top: Top
pending: Pending
flash_thread_new_error: Thread could not be created. Something went wrong.
+flash_thread_tag_banned_error: Thread could not be created. The content is not allowed.
flash_email_was_sent: Email has been successfully sent.
flash_email_failed_to_sent: Email could not be sent.
flash_post_new_success: Post has been successfully created.
From 60ebebd17e40124b4edd19fec87b56a0cc0651ac Mon Sep 17 00:00:00 2001
From: Melroy van den Berg
Date: Tue, 14 May 2024 20:14:35 +0200
Subject: [PATCH 009/335] Extend AI user agent bot ban list (#779)
---
public/robots.txt | 32 +++++++++++++++++++++++++++++++-
1 file changed, 31 insertions(+), 1 deletion(-)
diff --git a/public/robots.txt b/public/robots.txt
index 2d1e03d5a..65037c88a 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -1,10 +1,40 @@
-# Ban ChatGPT from indexing Mbin instances at all, in order to prevent training their [the OpenAI] models on users' data.
+# Ban several AI bots from indexing Mbin instances at all, in order to prevent training their models on users' data.
+
+# OpenAI, ChatGPT
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
+# Google AI (Gemini, etc)
+User-agent: Google-Extended
+Disallow: /
+
+# Block common crawl
+User-agent: CCBot
+Disallow: /
+
+# Facebook
+User-agent: FacebookBot
+Disallow: /
+
+# Cohere.ai
+User-agent: cohere-ai
+Disallow: /
+
+# Perplexity
+User-agent: PerplexityBot
+Disallow: /
+
+# Anthropic
+User-agent: anthropic-ai
+Disallow: /
+
+# ...also anthropic
+User-agent: ClaudeBot
+Disallow: /
+
# Rest of indexing robots
User-agent: *
Request-rate: 1/1s
From 3e9fb21662ef0a2b3d9386b0cd7926ca96aecb50 Mon Sep 17 00:00:00 2001
From: debounced <35878315+nobodyatroot@users.noreply.github.com>
Date: Tue, 14 May 2024 14:12:09 -0500
Subject: [PATCH 010/335] Add SimpleLogin SSO (#762)
Co-authored-by: e-five <146029455+e-five256@users.noreply.github.com>
---
.env.example | 2 +
.env.example_docker | 2 +
config/kbin_routes/security.yaml | 10 +
config/packages/knpu_oauth2_client.yaml | 7 +
config/packages/security.yaml | 1 +
config/services.yaml | 3 +
migrations/Version20240503224350.php | 29 +++
.../Security/SimpleLoginController.php | 28 +++
src/Entity/User.php | 4 +-
src/Provider/SimpleLogin.php | 72 +++++++
src/Provider/SimpleLoginResourceOwner.php | 58 ++++++
src/Security/SimpleLoginAuthenticator.php | 185 ++++++++++++++++++
src/Twig/Components/LoginSocialsComponent.php | 7 +
templates/components/login_socials.html.twig | 6 +-
14 files changed, 412 insertions(+), 2 deletions(-)
create mode 100644 migrations/Version20240503224350.php
create mode 100644 src/Controller/Security/SimpleLoginController.php
create mode 100644 src/Provider/SimpleLogin.php
create mode 100644 src/Provider/SimpleLoginResourceOwner.php
create mode 100644 src/Security/SimpleLoginAuthenticator.php
diff --git a/.env.example b/.env.example
index a551b36af..a27414961 100644
--- a/.env.example
+++ b/.env.example
@@ -75,6 +75,8 @@ OAUTH_KEYCLOAK_SECRET=
OAUTH_KEYCLOAK_URI=
OAUTH_KEYCLOAK_REALM=
OAUTH_KEYCLOAK_VERSION=
+OAUTH_SIMPLELOGIN_ID=
+OAUTH_SIMPLELOGIN_SECRET=
OAUTH_ZITADEL_ID=
OAUTH_ZITADEL_SECRET=
OAUTH_ZITADEL_BASE_URL=
diff --git a/.env.example_docker b/.env.example_docker
index 337c8a451..635c94f81 100644
--- a/.env.example_docker
+++ b/.env.example_docker
@@ -72,6 +72,8 @@ OAUTH_KEYCLOAK_SECRET=
OAUTH_KEYCLOAK_URI=
OAUTH_KEYCLOAK_REALM=
OAUTH_KEYCLOAK_VERSION=
+OAUTH_SIMPLELOGIN_ID=
+OAUTH_SIMPLELOGIN_SECRET=
OAUTH_ZITADEL_ID=
OAUTH_ZITADEL_SECRET=
OAUTH_ZITADEL_BASE_URL=
diff --git a/config/kbin_routes/security.yaml b/config/kbin_routes/security.yaml
index 11661e39a..897886ff1 100644
--- a/config/kbin_routes/security.yaml
+++ b/config/kbin_routes/security.yaml
@@ -93,6 +93,16 @@ oauth_keycloak_verify:
path: /oauth/keycloak/verify
methods: [ GET ]
+oauth_simplelogin_connect:
+ controller: App\Controller\Security\SimpleLoginController::connect
+ path: /oauth/simplelogin/connect
+ methods: [ GET ]
+
+oauth_simplelogin_verify:
+ controller: App\Controller\Security\SimpleLoginController::verify
+ path: /oauth/simplelogin/verify
+ methods: [ GET ]
+
oauth_zitadel_connect:
controller: App\Controller\Security\ZitadelController::connect
path: /oauth/zitadel/connect
diff --git a/config/packages/knpu_oauth2_client.yaml b/config/packages/knpu_oauth2_client.yaml
index d329673bf..bb547e394 100644
--- a/config/packages/knpu_oauth2_client.yaml
+++ b/config/packages/knpu_oauth2_client.yaml
@@ -35,6 +35,13 @@ knpu_oauth2_client:
version: '%oauth_keycloak_version%'
redirect_route: oauth_keycloak_verify
redirect_params: { }
+ simplelogin:
+ type: generic
+ client_id: '%oauth_simplelogin_id%'
+ client_secret: '%oauth_simplelogin_secret%'
+ redirect_route: oauth_simplelogin_verify
+ redirect_params: { }
+ provider_class: 'App\Provider\SimpleLogin'
zitadel:
type: generic
client_id: '%oauth_zitadel_id%'
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index b11113027..d77cda718 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -38,6 +38,7 @@ security:
- App\Security\GoogleAuthenticator
- App\Security\GithubAuthenticator
- App\Security\KeycloakAuthenticator
+ - App\Security\SimpleLoginAuthenticator
- App\Security\ZitadelAuthenticator
logout:
enable_csrf: true
diff --git a/config/services.yaml b/config/services.yaml
index 35a7c8ea1..f610510b3 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -53,6 +53,9 @@ parameters:
oauth_keycloak_realm: "%env(OAUTH_KEYCLOAK_REALM)%"
oauth_keycloak_version: "%env(OAUTH_KEYCLOAK_VERSION)%"
+ oauth_simplelogin_id: "%env(default::OAUTH_SIMPLELOGIN_ID)%"
+ oauth_simplelogin_secret: "%env(OAUTH_SIMPLELOGIN_SECRET)%"
+
oauth_zitadel_id: "%env(default::OAUTH_ZITADEL_ID)%"
oauth_zitadel_secret: "%env(OAUTH_ZITADEL_SECRET)%"
oauth_zitadel_base_url: "%env(OAUTH_ZITADEL_BASE_URL)%"
diff --git a/migrations/Version20240503224350.php b/migrations/Version20240503224350.php
new file mode 100644
index 000000000..9e2404ea0
--- /dev/null
+++ b/migrations/Version20240503224350.php
@@ -0,0 +1,29 @@
+addSql('ALTER TABLE "user" ADD oauth_simple_login_id VARCHAR(255) DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE "user" DROP oauth_simple_login_id');
+ }
+}
diff --git a/src/Controller/Security/SimpleLoginController.php b/src/Controller/Security/SimpleLoginController.php
new file mode 100644
index 000000000..94fb9ae65
--- /dev/null
+++ b/src/Controller/Security/SimpleLoginController.php
@@ -0,0 +1,28 @@
+getClient('simplelogin')
+ ->redirect([
+ 'openid',
+ 'email',
+ 'profile',
+ ]);
+ }
+
+ public function verify(Request $request, ClientRegistry $client)
+ {
+ }
+}
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 2d8c7674e..71dddb727 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -119,6 +119,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Visibil
#[Column(type: 'string', nullable: true)]
public ?string $oauthKeycloakId = null;
#[Column(type: 'string', nullable: true)]
+ public ?string $oauthSimpleLoginId = null;
+ #[Column(type: 'string', nullable: true)]
public ?string $oauthZitadelId = null;
#[Column(type: 'boolean', nullable: false, options: ['default' => true])]
public bool $hideAdult = true;
@@ -755,7 +757,7 @@ public function removeOAuth2UserConsent(OAuth2UserConsent $oAuth2UserConsent): s
public function isSsoControlled(): bool
{
- return $this->oauthAzureId || $this->oauthGithubId || $this->oauthGoogleId || $this->oauthFacebookId || $this->oauthKeycloakId || $this->oauthZitadelId;
+ return $this->oauthAzureId || $this->oauthGithubId || $this->oauthGoogleId || $this->oauthFacebookId || $this->oauthKeycloakId || $this->oauthSimpleLoginId || $this->oauthZitadelId;
}
public function getCustomCss(): ?string
diff --git a/src/Provider/SimpleLogin.php b/src/Provider/SimpleLogin.php
new file mode 100644
index 000000000..09145c51a
--- /dev/null
+++ b/src/Provider/SimpleLogin.php
@@ -0,0 +1,72 @@
+baseUrl, '/').'/';
+ }
+
+ protected function getAuthorizationHeaders($token = null)
+ {
+ return ['Authorization' => 'Bearer '.$token];
+ }
+
+ public function getBaseAuthorizationUrl()
+ {
+ return $this->getBaseUrl().'oauth2/authorize';
+ }
+
+ public function getBaseAccessTokenUrl(array $params)
+ {
+ return $this->getBaseUrl().'oauth2/token';
+ }
+
+ public function getResourceOwnerDetailsUrl(AccessToken $token)
+ {
+ return $this->getBaseUrl().'oauth2/userinfo';
+ }
+
+ protected function getDefaultScopes()
+ {
+ return ['openid', 'profile', 'email'];
+ }
+
+ protected function checkResponse(ResponseInterface $response, $data)
+ {
+ if (!empty($data['error'])) {
+ $error = htmlentities($data['error'], ENT_QUOTES, 'UTF-8');
+ $message = htmlentities($data['error_description'], ENT_QUOTES, 'UTF-8');
+ throw new IdentityProviderException($message, $response->getStatusCode(), $response);
+ }
+ }
+
+ protected function createResourceOwner(array $response, AccessToken $token)
+ {
+ return new SimpleLoginResourceOwner($response);
+ }
+
+ protected function getScopeSeparator()
+ {
+ return ' ';
+ }
+}
diff --git a/src/Provider/SimpleLoginResourceOwner.php b/src/Provider/SimpleLoginResourceOwner.php
new file mode 100644
index 000000000..a899ed6a0
--- /dev/null
+++ b/src/Provider/SimpleLoginResourceOwner.php
@@ -0,0 +1,58 @@
+response = $response;
+ }
+
+ public function getId()
+ {
+ return $this->getResponseValue('sub');
+ }
+
+ public function getName()
+ {
+ return $this->getResponseValue('name');
+ }
+
+ public function getEmail()
+ {
+ return $this->getResponseValue('email');
+ }
+
+ public function getPictureUrl()
+ {
+ return $this->getResponseValue('avatar_url');
+ }
+
+ public function toArray()
+ {
+ return $this->response;
+ }
+
+ protected function getResponseValue($key)
+ {
+ $keys = explode('.', $key);
+ $value = $this->response;
+
+ foreach ($keys as $k) {
+ if (isset($value[$k])) {
+ $value = $value[$k];
+ } else {
+ return null;
+ }
+ }
+
+ return $value;
+ }
+}
diff --git a/src/Security/SimpleLoginAuthenticator.php b/src/Security/SimpleLoginAuthenticator.php
new file mode 100644
index 000000000..32229ac79
--- /dev/null
+++ b/src/Security/SimpleLoginAuthenticator.php
@@ -0,0 +1,185 @@
+attributes->get('_route');
+ }
+
+ public function authenticate(Request $request): Passport
+ {
+ $client = $this->clientRegistry->getClient('simplelogin');
+ $slugger = $this->slugger;
+
+ $provider = $client->getOAuth2Provider();
+
+ $accessToken = $provider->getAccessToken('authorization_code', [
+ 'code' => $request->query->get('code'),
+ ]);
+
+ $rememberBadge = new RememberMeBadge();
+ $rememberBadge = $rememberBadge->enable();
+
+ return new SelfValidatingPassport(
+ new UserBadge($accessToken->getToken(), function () use ($accessToken, $client, $slugger) {
+ /** @var SimpleLoginResourceOwner $simpleloginUser */
+ $simpleloginUser = $client->fetchUserFromToken($accessToken);
+
+ $existingUser = $this->entityManager->getRepository(User::class)->findOneBy(
+ ['oauthSimpleLoginId' => $simpleloginUser->getId()]
+ );
+
+ if ($existingUser) {
+ return $existingUser;
+ }
+
+ $user = $this->userRepository->findOneBy(['email' => $simpleloginUser->getEmail()]);
+
+ if ($user) {
+ $user->oauthSimpleLoginId = $simpleloginUser->getId();
+
+ $this->entityManager->persist($user);
+ $this->entityManager->flush();
+
+ return $user;
+ }
+
+ if (false === $this->settingsManager->get('MBIN_SSO_REGISTRATIONS_ENABLED')) {
+ throw new CustomUserMessageAuthenticationException('MBIN_SSO_REGISTRATIONS_ENABLED');
+ }
+
+ $name = $simpleloginUser->getName();
+ $name = preg_replace('/\s+/', '', $name); // remove all whitespace
+ $name = preg_replace('#[[:punct:]]#', '', $name); // remove all punctuation
+
+ $username = $slugger->slug($name);
+
+ $usernameTaken = $this->entityManager->getRepository(User::class)->findOneBy(
+ ['username' => $username]
+ );
+
+ if ($usernameTaken) {
+ $username = $username.rand(1, 999);
+ }
+
+ $dto = (new UserDto())->create(
+ $username,
+ $simpleloginUser->getEmail()
+ );
+
+ $avatar = $this->getAvatar($simpleloginUser->getPictureUrl());
+
+ if ($avatar) {
+ $dto->avatar = $this->imageFactory->createDto($avatar);
+ }
+
+ $dto->plainPassword = bin2hex(random_bytes(20));
+ $dto->ip = $this->ipResolver->resolve();
+
+ $user = $this->userManager->create($dto, false);
+ $user->oauthSimpleLoginId = $simpleloginUser->getId();
+ $user->avatar = $this->getAvatar($simpleloginUser->getPictureUrl());
+ $user->isVerified = true;
+
+ $this->entityManager->persist($user);
+ $this->entityManager->flush();
+
+ return $user;
+ }),
+ [
+ $rememberBadge,
+ ]
+ );
+ }
+
+ private function getAvatar(?string $pictureUrl): ?Image
+ {
+ if (!$pictureUrl) {
+ return null;
+ }
+
+ try {
+ $tempFile = $this->imageManager->download($pictureUrl);
+ } catch (\Exception $e) {
+ $tempFile = null;
+ }
+
+ if ($tempFile) {
+ $image = $this->imageRepository->findOrCreateFromPath($tempFile);
+ if ($image) {
+ $this->entityManager->persist($image);
+ $this->entityManager->flush();
+ }
+ }
+
+ return $image ?? null;
+ }
+
+ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
+ {
+ $targetUrl = $this->router->generate('user_settings_profile');
+
+ return new RedirectResponse($targetUrl);
+ }
+
+ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
+ {
+ $message = strtr($exception->getMessageKey(), $exception->getMessageData());
+
+ if ('MBIN_SSO_REGISTRATIONS_ENABLED' === $message) {
+ $session = $request->getSession();
+ $session->getFlashBag()->add('error', 'sso_registrations_enabled.error');
+
+ return new RedirectResponse($this->router->generate('app_login'));
+ }
+
+ return new Response($message, Response::HTTP_FORBIDDEN);
+ }
+}
diff --git a/src/Twig/Components/LoginSocialsComponent.php b/src/Twig/Components/LoginSocialsComponent.php
index d9772fc04..9f1979b83 100644
--- a/src/Twig/Components/LoginSocialsComponent.php
+++ b/src/Twig/Components/LoginSocialsComponent.php
@@ -19,6 +19,8 @@ public function __construct(
private readonly ?string $oauthGithubId,
#[Autowire('%oauth_keycloak_id%')]
private readonly ?string $oauthKeycloakId,
+ #[Autowire('%oauth_simplelogin_id%')]
+ private readonly ?string $oauthSimpleLoginId,
#[Autowire('%oauth_zitadel_id%')]
private readonly ?string $oauthZitadelId,
#[Autowire('%oauth_azure_id%')]
@@ -46,6 +48,11 @@ public function keycloakEnabled(): bool
return !empty($this->oauthKeycloakId);
}
+ public function simpleloginEnabled(): bool
+ {
+ return !empty($this->oauthSimpleLoginId);
+ }
+
public function zitadelEnabled(): bool
{
return !empty($this->oauthZitadelId);
diff --git a/templates/components/login_socials.html.twig b/templates/components/login_socials.html.twig
index 117a89758..116ea6578 100644
--- a/templates/components/login_socials.html.twig
+++ b/templates/components/login_socials.html.twig
@@ -1,5 +1,5 @@
{# @var this App\Twig\Components\LoginSocialsComponent #}
-{%- set HAS_ANY_SOCIAL = this.googleEnabled or this.facebookEnabled or this.githubEnabled or this.keycloakEnabled or this.zitadelEnabled or this.azureEnabled -%}
+{%- set HAS_ANY_SOCIAL = this.googleEnabled or this.facebookEnabled or this.githubEnabled or this.keycloakEnabled or this.simpleloginEnabled or this.zitadelEnabled or this.azureEnabled -%}
{% if HAS_ANY_SOCIAL %}
{% if not mbin_sso_only_mode() and not mbin_sso_show_first() %}
@@ -21,6 +21,10 @@
{{ 'continue_with'|trans }} Keycloak
{% endif %}
+ {% if this.simpleloginEnabled %}
+
+ {{ 'continue_with'|trans }} SimpleLogin
+ {% endif %}
{% if this.zitadelEnabled %}
{{ 'continue_with'|trans }} Zitadel
From 0ca9585fc9ebd332d741e53800476803519ad5e3 Mon Sep 17 00:00:00 2001
From: debounced <35878315+nobodyatroot@users.noreply.github.com>
Date: Tue, 14 May 2024 20:23:37 -0500
Subject: [PATCH 011/335] Add available simple-icons to SimpleLogin SSO (#781)
---
assets/styles/app.scss | 1 +
package-lock.json | 11 +++++++++++
package.json | 1 +
templates/components/login_socials.html.twig | 2 +-
4 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/assets/styles/app.scss b/assets/styles/app.scss
index c849d3343..7f60d159a 100644
--- a/assets/styles/app.scss
+++ b/assets/styles/app.scss
@@ -1,6 +1,7 @@
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/brands';
+@import 'simple-icons-font/font/simple-icons';
@import 'glightbox/dist/css/glightbox.min.css';
@import 'mixins/animations';
@import 'mixins/kbin';
diff --git a/package-lock.json b/package-lock.json
index e93247403..08ec93b67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"regenerator-runtime": "^0.14.0",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
+ "simple-icons-font": "^11.12.0",
"stimulus-textarea-autogrow": "^4.1.0",
"stimulus-use": "^0.52.1",
"timeago.js": "^4.0.2",
@@ -9689,6 +9690,16 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
+ "node_modules/simple-icons-font": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/simple-icons-font/-/simple-icons-font-11.12.0.tgz",
+ "integrity": "sha512-JFdVtHwh5513JklPWwCvFp0EpwOPRhQNUy9/Eaudgfhf0FD6CqN26EvFGiS24WCE2dEG28ZwUpliiZK5QID2gQ==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/simple-icons"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
diff --git a/package.json b/package.json
index 4e8e03d89..35fe6e716 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"regenerator-runtime": "^0.14.0",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
+ "simple-icons-font": "^11.12.0",
"stimulus-textarea-autogrow": "^4.1.0",
"stimulus-use": "^0.52.1",
"timeago.js": "^4.0.2",
diff --git a/templates/components/login_socials.html.twig b/templates/components/login_socials.html.twig
index 116ea6578..fd94e4987 100644
--- a/templates/components/login_socials.html.twig
+++ b/templates/components/login_socials.html.twig
@@ -22,7 +22,7 @@
{{ 'continue_with'|trans }} Keycloak
{% endif %}
{% if this.simpleloginEnabled %}
-
+ {{ 'continue_with'|trans }} SimpleLogin
{% endif %}
{% if this.zitadelEnabled %}
From 5431dc34bc515a460d787d628ffd6edacec6dc79 Mon Sep 17 00:00:00 2001
From: asdfzdfj
Date: Wed, 15 May 2024 11:35:03 +0700
Subject: [PATCH 012/335] front controller path/routing adjustment (#687)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
the new filter ui changes also introduces a couple more filtering
parameters in the form of controller path params, for threads/entries
listing it looks fine and relatively staightforward, but for microblog
posts listing, what used to be a simple `/microblog` is now something
like `/all/hot/∞/all/all/microblog`, which hinders the user experience
on url readability and memorability compared to the previous scheme, for
those who still care
this changes tries to adjust the path/routing of these front controller
especially for the microblog part, to make it more simpler and could
omit some default filter parameters if they aren't set, similar to the
previous path scheme
Co-authored-by: e-five <146029455+e-five256@users.noreply.github.com>
---
config/kbin_routes/domain.yaml | 5 +-
config/kbin_routes/front.yaml | 77 ++++----
.../Domain/DomainFrontController.php | 11 +-
src/Controller/Entry/EntryFrontController.php | 166 ++++++++++++------
src/Repository/Criteria.php | 23 +--
src/Twig/Extension/FrontExtension.php | 19 ++
src/Twig/Runtime/FrontExtensionRuntime.php | 64 +++++++
src/Twig/Runtime/UrlExtensionRuntime.php | 2 +-
templates/domain/_options.html.twig | 2 +-
templates/entry/_options.html.twig | 50 +++---
templates/layout/_header_nav.html.twig | 8 +-
templates/post/_options.html.twig | 50 +++---
12 files changed, 313 insertions(+), 164 deletions(-)
create mode 100644 src/Twig/Extension/FrontExtension.php
create mode 100644 src/Twig/Runtime/FrontExtensionRuntime.php
diff --git a/config/kbin_routes/domain.yaml b/config/kbin_routes/domain.yaml
index d209af86b..6c0631049 100644
--- a/config/kbin_routes/domain.yaml
+++ b/config/kbin_routes/domain.yaml
@@ -1,12 +1,11 @@
domain_entries:
controller: App\Controller\Domain\DomainFrontController
- defaults: { sortBy: hot, time: '∞', type: ~ }
- path: /d/{name}/{sortBy}/{time}/{type}
+ defaults: { sortBy: hot, time: '∞'}
+ path: /d/{name}/{sortBy}/{time}
methods: [ GET ]
requirements:
sortBy: "%default_sort_options%"
time: "%default_time_options%"
- type: "%default_type_options%"
domain_comments:
controller: App\Controller\Domain\DomainCommentFrontController
diff --git a/config/kbin_routes/front.yaml b/config/kbin_routes/front.yaml
index ef14c5b5f..01a28a07d 100644
--- a/config/kbin_routes/front.yaml
+++ b/config/kbin_routes/front.yaml
@@ -1,45 +1,55 @@
front:
controller: App\Controller\Entry\EntryFrontController::front
- defaults: { subscription: home, sortBy: hot, time: '∞', type: all, federation: all, content: threads }
- path: /{subscription}/{sortBy}/{time}/{type}/{federation}/{content}
+ defaults: &front_defaults { subscription: home, content: threads, sortBy: hot, time: '∞', federation: all }
+ path: /{subscription}/{content}/{sortBy}/{time}/{federation}
methods: [GET]
- requirements:
+ requirements: &front_requirement
subscription: "%default_subscription_options%"
sortBy: "%default_sort_options%"
time: "%default_time_options%"
- type: "%default_type_options%"
federation: "%default_federation_options%"
content: "%default_content_options%"
-front_redirect:
- controller: App\Controller\Entry\EntryFrontController::front_redirect
- defaults: { sortBy: hot, time: '∞', type: all, federation: all, content: threads }
- path: /{sortBy}/{time}/{type}/{federation}/{content}
+front_sub:
+ controller: App\Controller\Entry\EntryFrontController::front
+ defaults: *front_defaults
+ path: /{subscription}/{sortBy}/{time}/{federation}
methods: [GET]
- requirements:
- sortBy: "%default_sort_options%"
- time: "%default_time_options%"
- type: "%default_type_options%"
- federation: "%default_federation_options%"
- content: "%default_content_options%"
+ requirements: *front_requirement
+
+front_content:
+ controller: App\Controller\Entry\EntryFrontController::front
+ defaults: *front_defaults
+ path: /{content}/{sortBy}/{time}/{federation}
+ methods: [GET]
+ requirements: *front_requirement
+
+front_short:
+ controller: App\Controller\Entry\EntryFrontController::front
+ defaults: *front_defaults
+ path: /{sortBy}/{time}/{federation}
+ methods: [GET]
+ requirements: *front_requirement
front_magazine:
controller: App\Controller\Entry\EntryFrontController::magazine
- defaults: { sortBy: hot, time: '∞', type: all, federation: all, content: threads }
- path: /m/{name}/{sortBy}/{time}/{type}/{federation}/{content}
+ defaults: &front_magazine_defaults { content: threads, sortBy: hot, time: '∞', federation: all }
+ path: /m/{name}/{content}/{sortBy}/{time}/{federation}
methods: [GET]
- requirements:
- sortBy: "%default_sort_options%"
- time: "%default_time_options%"
- type: "%default_type_options%"
- federation: "%default_federation_options%"
- content: "%default_content_options%"
+ requirements: *front_requirement
+
+front_magazine_short:
+ controller: App\Controller\Entry\EntryFrontController::magazine
+ defaults: &front_magazine_defaults
+ path: /m/{name}/{sortBy}/{time}/{federation}
+ methods: [GET]
+ requirements: *front_requirement
# Microblog compatibility stuff, redirects from the old routes' URLs
posts_front:
- controller: App\Controller\Entry\EntryFrontController::front_redirect
- defaults: { sortBy: hot, time: '∞', type: all, federation: all, content: microblog }
+ controller: App\Controller\Entry\EntryFrontController::frontRedirect
+ defaults: { sortBy: hot, time: '∞', federation: all, content: microblog }
path: /microblog/{sortBy}/{time}
methods: [ GET ]
requirements:
@@ -47,8 +57,8 @@ posts_front:
time: "%default_time_options%"
posts_subscribed:
- controller: App\Controller\Entry\EntryFrontController::front_redirect
- defaults: { sortBy: hot, time: '∞', type: all, federation: all, content: microblog, subscription: 'sub' }
+ controller: App\Controller\Entry\EntryFrontController::frontRedirect
+ defaults: { sortBy: hot, time: '∞', federation: all, content: microblog, subscription: 'sub' }
path: /sub/microblog/{sortBy}/{time}
methods: [ GET ]
requirements:
@@ -56,8 +66,8 @@ posts_subscribed:
time: "%default_time_options%"
posts_moderated:
- controller: App\Controller\Entry\EntryFrontController::front_redirect
- defaults: { sortBy: hot, time: '∞', type: all, federation: all, content: microblog, subscription: 'mod' }
+ controller: App\Controller\Entry\EntryFrontController::frontRedirect
+ defaults: { sortBy: hot, time: '∞', federation: all, content: microblog, subscription: 'mod' }
path: /mod/microblog/{sortBy}/{time}
methods: [ GET ]
requirements:
@@ -65,8 +75,8 @@ posts_moderated:
time: "%default_time_options%"
posts_favourite:
- controller: App\Controller\Entry\EntryFrontController::front_redirect
- defaults: { sortBy: hot, time: '∞', type: all, federation: all, content: microblog, subscription: 'fav' }
+ controller: App\Controller\Entry\EntryFrontController::frontRedirect
+ defaults: { sortBy: hot, time: '∞', federation: all, content: microblog, subscription: 'fav' }
path: /fav/microblog/{sortBy}/{time}
methods: [ GET ]
requirements:
@@ -74,12 +84,11 @@ posts_favourite:
time: "%default_time_options%"
magazine_posts:
- controller: App\Controller\Entry\EntryFrontController::front_redirect
- defaults: { sortBy: hot, time: '∞', type: all, federation: all, content: microblog }
- path: /m/{name}/microblog/{sortBy}/{time}/{type}/{federation}
+ controller: App\Controller\Entry\EntryFrontController::magazineRedirect
+ defaults: { sortBy: hot, time: '∞', federation: all, content: microblog }
+ path: /m/{name}/microblog/{sortBy}/{time}/{federation}
methods: [ GET ]
requirements:
sortBy: "%default_sort_options%"
time: "%default_time_options%"
- type: "%default_type_options%"
federation: "%default_federation_options%"
diff --git a/src/Controller/Domain/DomainFrontController.php b/src/Controller/Domain/DomainFrontController.php
index 54d2b5714..167f336eb 100644
--- a/src/Controller/Domain/DomainFrontController.php
+++ b/src/Controller/Domain/DomainFrontController.php
@@ -13,6 +13,7 @@
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
class DomainFrontController extends AbstractController
{
@@ -22,8 +23,14 @@ public function __construct(
) {
}
- public function __invoke(?string $name, ?string $sortBy, ?string $time, ?string $type, Request $request): Response
- {
+ public function __invoke(
+ ?string $name,
+ ?string $sortBy,
+ ?string $time,
+ #[MapQueryParameter]
+ ?string $type,
+ Request $request
+ ): Response {
if (!$domain = $this->domainRepository->findOneBy(['name' => $name])) {
throw $this->createNotFoundException();
}
diff --git a/src/Controller/Entry/EntryFrontController.php b/src/Controller/Entry/EntryFrontController.php
index 6e290e612..d9cf2112a 100644
--- a/src/Controller/Entry/EntryFrontController.php
+++ b/src/Controller/Entry/EntryFrontController.php
@@ -19,15 +19,26 @@
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
class EntryFrontController extends AbstractController
{
- public function __construct(private readonly EntryRepository $entryRepository, private readonly PostRepository $postRepository)
- {
+ public function __construct(
+ private readonly EntryRepository $entryRepository,
+ private readonly PostRepository $postRepository
+ ) {
}
- public function front(?string $sortBy, ?string $time, ?string $type, string $subscription, string $federation, string $content, Request $request): Response
- {
+ public function front(
+ string $subscription,
+ string $content,
+ ?string $sortBy,
+ ?string $time,
+ string $federation,
+ #[MapQueryParameter]
+ ?string $type,
+ Request $request
+ ): Response {
$user = $this->getUser();
$criteria = $this->createCriteria($content, $request);
@@ -39,57 +50,64 @@ public function front(?string $sortBy, ?string $time, ?string $type, string $sub
if ('home' === $subscription) {
$subscription = $this->subscriptionFor($user);
}
- $this->handleSubscription($subscription, $user, $criteria);
+ $this->handleSubscription($subscription, $criteria);
$this->setUserPreferences($user, $criteria);
- $entities = ('threads' === $content) ? $this->entryRepository->findByCriteria($criteria) : $this->postRepository->findByCriteria($criteria);
if ('threads' === $content) {
+ $entities = $this->entryRepository->findByCriteria($criteria);
$entities = $this->handleCrossposts($entities);
+ $templatePath = 'entry/';
+ $dataKey = 'entries';
+ } elseif ('microblog' === $content) {
+ $entities = $this->postRepository->findByCriteria($criteria);
+ $templatePath = 'post/';
+ $dataKey = 'posts';
+ } else {
+ throw new \LogicException("Invalid content filter '{$content}'");
}
- $templatePath = ('threads' === $content) ? 'entry/' : 'post/';
- $dataKey = ('threads' === $content) ? 'entries' : 'posts';
-
- return $this->renderResponse($request, $content, $criteria, [$dataKey => $entities], $templatePath, $user);
+ return $this->renderResponse(
+ $request,
+ $content,
+ $criteria,
+ [$dataKey => $entities],
+ $templatePath,
+ $user
+ );
}
- // $name is magazine name, for compatibility
- public function front_redirect(?string $sortBy, ?string $time, ?string $type, string $federation, string $content, ?string $name, Request $request): Response
- {
- $user = $this->getUser(); // Fetch the user
- $subscription = $this->subscriptionFor($user); // Determine the subscription filter based on the user
-
- if ($name) {
- return $this->redirectToRoute('front_magazine', [
- 'name' => $name,
- 'subscription' => $subscription,
- 'sortBy' => $sortBy,
- 'time' => $time,
- 'type' => $type,
- 'federation' => $federation,
- 'content' => $content,
- ]);
- } else {
- return $this->redirectToRoute('front', [
- 'subscription' => $subscription,
- 'sortBy' => $sortBy,
- 'time' => $time,
- 'type' => $type,
- 'federation' => $federation,
- 'content' => $content,
- ]);
- }
+ public function frontRedirect(
+ string $content,
+ ?string $sortBy,
+ ?string $time,
+ string $federation,
+ #[MapQueryParameter]
+ ?string $type,
+ Request $request
+ ): Response {
+ $user = $this->getUser();
+ $subscription = $this->subscriptionFor($user);
+
+ return $this->redirectToRoute('front', [
+ 'subscription' => $subscription,
+ 'sortBy' => $sortBy,
+ 'time' => $time,
+ 'type' => $type,
+ 'federation' => $federation,
+ 'content' => $content,
+ ]);
}
public function magazine(
#[MapEntity(expr: 'repository.findOneByName(name)')]
Magazine $magazine,
+ string $content,
?string $sortBy,
?string $time,
- ?string $type,
string $federation,
- string $content,
+ #[MapQueryParameter]
+ ?string $type,
Request $request
): Response {
$user = $this->getUser();
@@ -106,21 +124,59 @@ public function magazine(
$criteria->magazine = $magazine;
$criteria->stickiesFirst = true;
- $subscription = $request->query->get('subscription');
- if (!$subscription) {
- $subscription = 'all';
- }
- $this->handleSubscription($subscription, $user, $criteria);
+ $subscription = $request->query->get('subscription') ?: 'all';
+ $this->handleSubscription($subscription, $criteria);
$this->setUserPreferences($user, $criteria);
- $entities = ('threads' === $content) ? $this->entryRepository->findByCriteria($criteria) : $this->postRepository->findByCriteria($criteria);
- // Note no crosspost handling
+ if ('threads' === $content) {
+ $entities = $this->entryRepository->findByCriteria($criteria);
+ // Note no crosspost handling
+ $templatePath = 'entry/';
+ $dataKey = 'entries';
+ } elseif ('microblog' === $content) {
+ $entities = $this->postRepository->findByCriteria($criteria);
+ $templatePath = 'post/';
+ $dataKey = 'posts';
+ } else {
+ throw new \LogicException("Invalid content filter '{$content}'");
+ }
+
+ return $this->renderResponse(
+ $request,
+ $content,
+ $criteria,
+ [$dataKey => $entities, 'magazine' => $magazine],
+ $templatePath,
+ $user
+ );
+ }
- $templatePath = ('threads' === $content) ? 'entry/' : 'post/';
- $dataKey = ('threads' === $content) ? 'entries' : 'posts';
+ /**
+ * @param string $name magazine name
+ */
+ public function magazineRedirect(
+ string $name,
+ string $content,
+ ?string $sortBy,
+ ?string $time,
+ string $federation,
+ #[MapQueryParameter]
+ ?string $type,
+ Request $request
+ ): Response {
+ $user = $this->getUser(); // Fetch the user
+ $subscription = $this->subscriptionFor($user); // Determine the subscription filter based on the user
- return $this->renderResponse($request, $content, $criteria, [$dataKey => $entities, 'magazine' => $magazine], $templatePath, $user);
+ return $this->redirectToRoute('front_magazine', [
+ 'name' => $name,
+ 'subscription' => $subscription,
+ 'sortBy' => $sortBy,
+ 'time' => $time,
+ 'type' => $type,
+ 'federation' => $federation,
+ 'content' => $content,
+ ]);
}
private function createCriteria(string $content, Request $request)
@@ -136,19 +192,18 @@ private function createCriteria(string $content, Request $request)
return $criteria->setContent($content);
}
- private function handleSubscription(string $subscription, $user, &$criteria)
+ private function handleSubscription(string $subscription, &$criteria)
{
- if ('sub' === $subscription) {
+ if (\in_array($subscription, ['sub', 'mod', 'fav'])) {
$this->denyAccessUnlessGranted('ROLE_USER');
$this->getUserOrThrow();
+ }
+
+ if ('sub' === $subscription) {
$criteria->subscribed = true;
} elseif ('mod' === $subscription) {
- $this->denyAccessUnlessGranted('ROLE_USER');
- $this->getUserOrThrow();
$criteria->moderated = true;
} elseif ('fav' === $subscription) {
- $this->denyAccessUnlessGranted('ROLE_USER');
- $this->getUserOrThrow();
$criteria->favourite = true;
} elseif ($subscription && 'all' !== $subscription) {
throw new \LogicException('Invalid subscription filter '.$subscription);
@@ -164,7 +219,8 @@ private function setUserPreferences(?User $user, &$criteria)
private function renderResponse(Request $request, $content, $criteria, $data, $templatePath, ?User $user)
{
- $baseData = ['criteria' => $criteria] + $data;
+ $baseData = array_merge(['criteria' => $criteria], $data);
+
if ('microblog' === $content) {
$dto = new PostDto();
if (isset($data['magazine'])) {
diff --git a/src/Repository/Criteria.php b/src/Repository/Criteria.php
index c9edd8a94..747eda64c 100644
--- a/src/Repository/Criteria.php
+++ b/src/Repository/Criteria.php
@@ -230,22 +230,13 @@ public function resolveTime(?string $value, bool $reverse = false): ?string
public function resolveType(?string $value): ?string
{
- // @todo
- $routes = [
- 'all' => 'all',
- 'article' => Entry::ENTRY_TYPE_ARTICLE,
- 'articles' => Entry::ENTRY_TYPE_ARTICLE,
- 'link' => Entry::ENTRY_TYPE_LINK,
- 'links' => Entry::ENTRY_TYPE_LINK,
- 'video' => Entry::ENTRY_TYPE_VIDEO,
- 'videos' => Entry::ENTRY_TYPE_VIDEO,
- 'photo' => Entry::ENTRY_TYPE_IMAGE,
- 'photos' => Entry::ENTRY_TYPE_IMAGE,
- 'image' => Entry::ENTRY_TYPE_IMAGE,
- 'images' => Entry::ENTRY_TYPE_IMAGE,
- ];
-
- return $routes[$value] ?? 'all';
+ return match ($value) {
+ 'article', 'articles' => Entry::ENTRY_TYPE_ARTICLE,
+ 'link', 'links' => Entry::ENTRY_TYPE_LINK,
+ 'video', 'videos' => Entry::ENTRY_TYPE_VIDEO,
+ 'photo', 'photos', 'image', 'images' => Entry::ENTRY_TYPE_IMAGE,
+ default => 'all'
+ };
}
public function translateType(): string
diff --git a/src/Twig/Extension/FrontExtension.php b/src/Twig/Extension/FrontExtension.php
new file mode 100644
index 000000000..04554e3f9
--- /dev/null
+++ b/src/Twig/Extension/FrontExtension.php
@@ -0,0 +1,19 @@
+requestStack->getCurrentRequest();
+ $attrs = $request->attributes;
+ $route = $routeName ?? $attrs->get('_route');
+
+ $params = array_merge($attrs->get('_route_params', []), $request->query->all());
+ $params = array_replace($params, $additionalParams);
+ $params = array_filter($params, fn ($v) => null !== $v);
+
+ $params[$name] = $value;
+
+ if (str_starts_with($route, 'front') && !str_contains($route, '_magazine')) {
+ $route = $this->getFrontRoute($route, $params);
+ }
+
+ return $this->urlGenerator->generate($route, $params);
+ }
+
+ /**
+ * Upgrades shorter `front_*` routes to a front route that can fit all specified params.
+ */
+ private function getFrontRoute(string $currentRoute, array $params): string
+ {
+ $content = $params['content'] ?? null;
+ $subscription = $params['subscription'] ?? null;
+
+ if (\in_array($currentRoute, ['front_sub', 'front_content']) && $content && $subscription) {
+ return 'front';
+ } elseif ('front_short' === $currentRoute) {
+ return match (true) {
+ !empty($content) => 'front_content',
+ !empty($subscription) => 'front_sub',
+ default => 'front',
+ };
+ }
+
+ return 'front';
+ }
+}
diff --git a/src/Twig/Runtime/UrlExtensionRuntime.php b/src/Twig/Runtime/UrlExtensionRuntime.php
index 54a13661c..46d19859b 100644
--- a/src/Twig/Runtime/UrlExtensionRuntime.php
+++ b/src/Twig/Runtime/UrlExtensionRuntime.php
@@ -265,7 +265,7 @@ public function postCommentDeleteUrl(PostComment $comment): string
// $additionalParams indicates extra parameters to set in addition to [$name] = $value
// Set $value to null to indicate deleting a parameter
// TODO: It'd be better to have just a single $params which is an associative array
- public function optionsUrl(string $name, string $value, string $routeName = null, array $additionalParams = []): string
+ public function optionsUrl(string $name, ?string $value, string $routeName = null, array $additionalParams = []): string
{
$route = $routeName ?? $this->requestStack->getCurrentRequest()->attributes->get('_route');
$params = $this->requestStack->getCurrentRequest()->attributes->get('_route_params', []);
diff --git a/templates/domain/_options.html.twig b/templates/domain/_options.html.twig
index 65bd7c990..c50c3326c 100644
--- a/templates/domain/_options.html.twig
+++ b/templates/domain/_options.html.twig
@@ -124,7 +124,7 @@
Date: Fri, 14 Jun 2024 15:07:29 +0200
Subject: [PATCH 040/335] Translations update from Hosted Weblate (#829)
Co-authored-by: BentiGorlich
---
translations/messages.de.yaml | 23 ++++++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/translations/messages.de.yaml b/translations/messages.de.yaml
index d6754323a..b056016d9 100644
--- a/translations/messages.de.yaml
+++ b/translations/messages.de.yaml
@@ -422,8 +422,7 @@ toolbar.ordered_list: Geordnete Liste
your_account_is_not_active: Dein Profil wurde noch nicht aktiviert. Bitte prüfe deine
E-Mails und klicke den Aktivierungslink um fortzufahren. Falls du keine Mail erhalten
hast frage eine neue Aktivierungsmail an.
-your_account_has_been_banned: E-Mails für Anweisungen zur Kontoaktivierung oder fordere eine neue E-Mail zur Aktivierung an
+your_account_has_been_banned: Dein Konto wurde gesperrt
federation_page_enabled: Föderationsseite aktiviert
federation_page_disallowed_description: Instanzen mit denen wir nicht föderieren
federation_page_allowed_description: Bekannte Instanzen mit denen wir föderieren
@@ -469,7 +468,7 @@ subscription_sort: Sortierung
unblock: Entblockieren
oauth.consent.grant_permissions: Gebe Berechtigungen
oauth2.grant.moderate.magazine.ban.delete: Nutzer in deinen moderierten Magazinen
- entbannen.
+ entsperren.
subscriptions_in_own_sidebar: In eigener Seitenleiste
subscription_sidebar_pop_out_left: Nach links in eigene Seitenleiste verschieben
subscription_sidebar_pop_out_right: Nach rechts in eigene Seitenleiste verschieben
@@ -854,3 +853,21 @@ restrict_magazine_creation: 'Erstellung lokaler Magazine auf Admins und globale
sort_by: 'Sortieren nach'
filter_by_subscription: 'Nach Abonnements filtern'
related_entry: Zugehörig
+tag: Hashtag
+unban: Sperre aufheben
+ban_hashtag_btn: Hashtag Sperren
+unban_hashtag_btn: Hashtag Sperre Aufheben
+private_instance: Nutzer zur Anmeldung zwingen um auf Inhalte zugreifen zu können
+flash_thread_tag_banned_error: Thema konnte nicht erstellt werden. Der Inhalt ist
+ nicht erlaubt.
+sso_only_mode: Anmeldung und Registrierung auf SSO Methoden beschränken
+magazine_log_mod_added: hat einen Moderator hinzugefügt
+magazine_log_mod_removed: hat einen Moderator entfernt
+ban_hashtag_description: Durch das Sperren eines Hashtags wird verhindert, dass Beiträge
+ mit diesem Hashtag erstellt werden. Außerdem werden vorhandene Beiträge mit diesem
+ Hashtag ausgeblendet.
+unban_hashtag_description: Wenn Sie eine Hashtag Sperre aufheben, können wieder Beiträge
+ mit diesem Hashtag erstellt werden. Vorhandene Beiträge mit diesem Hashtag werden
+ nicht mehr ausgeblendet.
+sso_show_first: SSO als erstes auf der Anmeldungs- und Registrierungsseite anzeigen
+continue_with: Weiter mit
From 88800b183973276a0ffbe228606d884f6487df30 Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Fri, 14 Jun 2024 13:28:32 +0000
Subject: [PATCH 041/335] New/cake day (#827)
---
src/Service/ActivityPubManager.php | 22 +++++++++++++++++++
src/Service/SettingsManager.php | 9 ++++++++
src/Twig/Extension/SettingsExtension.php | 1 +
src/Twig/Runtime/SettingsExtensionRuntime.php | 5 +++++
templates/user/_info.html.twig | 10 ++++++---
templates/user/_user_popover.html.twig | 4 ++++
translations/messages.en.yaml | 1 +
7 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php
index fda2ec4cc..bb1b2a60d 100644
--- a/src/Service/ActivityPubManager.php
+++ b/src/Service/ActivityPubManager.php
@@ -330,6 +330,17 @@ public function updateUser(string $actorUrl): ?User
$user->apTimeoutAt = null;
$user->apFetchedAt = new \DateTime();
+ if (isset($actor['published'])) {
+ try {
+ $createdAt = new \DateTimeImmutable($actor['published']);
+ $now = new \DateTimeImmutable();
+ if ($createdAt < $now) {
+ $user->createdAt = $createdAt;
+ }
+ } catch (\Exception) {
+ }
+ }
+
// Only update about when summary is set
if (isset($actor['summary'])) {
$converter = new HtmlConverter(['strip_tags' => true]);
@@ -463,6 +474,17 @@ public function updateMagazine(string $actorUrl): ?Magazine
$magazine->title = $actor['preferredUsername'];
}
+ if (isset($actor['published'])) {
+ try {
+ $createdAt = new \DateTimeImmutable($actor['published']);
+ $now = new \DateTimeImmutable();
+ if ($createdAt < $now) {
+ $magazine->createdAt = $createdAt;
+ }
+ } catch (\Exception) {
+ }
+ }
+
$magazine->apInboxUrl = $actor['endpoints']['sharedInbox'] ?? $actor['inbox'];
$magazine->apDomain = parse_url($actor['id'], PHP_URL_HOST);
$magazine->apFollowersUrl = $actor['followers'] ?? null;
diff --git a/src/Service/SettingsManager.php b/src/Service/SettingsManager.php
index 11ac26645..bcd672951 100644
--- a/src/Service/SettingsManager.php
+++ b/src/Service/SettingsManager.php
@@ -9,6 +9,7 @@
use App\Repository\SettingsRepository;
use Doctrine\ORM\EntityManagerInterface;
use JetBrains\PhpStorm\Pure;
+use Symfony\Component\HttpFoundation\RequestStack;
class SettingsManager
{
@@ -17,6 +18,7 @@ class SettingsManager
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly SettingsRepository $repository,
+ private readonly RequestStack $requestStack,
private readonly string $kbinDomain,
private readonly string $kbinTitle,
private readonly string $kbinMetaTitle,
@@ -153,4 +155,11 @@ public static function getValue(string $name): string
{
return self::$dto->{$name};
}
+
+ public function getLocale(): string
+ {
+ $request = $this->requestStack->getCurrentRequest();
+
+ return $request->cookies->get('kbin_lang') ?? $request->getLocale() ?? $this->get('KBIN_DEFAULT_LANG');
+ }
}
diff --git a/src/Twig/Extension/SettingsExtension.php b/src/Twig/Extension/SettingsExtension.php
index c3981962e..1b966c0c3 100644
--- a/src/Twig/Extension/SettingsExtension.php
+++ b/src/Twig/Extension/SettingsExtension.php
@@ -31,6 +31,7 @@ public function getFunctions(): array
new TwigFunction('mbin_restrict_magazine_creation', [SettingsExtensionRuntime::class, 'mbinRestrictMagazineCreation']),
new TwigFunction('mbin_private_instance', [SettingsExtensionRuntime::class, 'mbinPrivateInstance']),
new TwigFunction('mbin_sso_show_first', [SettingsExtensionRuntime::class, 'mbinSsoShowFirst']),
+ new TwigFunction('mbin_lang', [SettingsExtensionRuntime::class, 'mbinLang']),
];
}
}
diff --git a/src/Twig/Runtime/SettingsExtensionRuntime.php b/src/Twig/Runtime/SettingsExtensionRuntime.php
index 6f5ce0ea5..2213d98fc 100644
--- a/src/Twig/Runtime/SettingsExtensionRuntime.php
+++ b/src/Twig/Runtime/SettingsExtensionRuntime.php
@@ -118,4 +118,9 @@ public function mbinSsoShowFirst(): bool
{
return $this->settings->get('MBIN_SSO_SHOW_FIRST');
}
+
+ public function mbinLang(): string
+ {
+ return $this->settings->getLocale();
+ }
}
diff --git a/templates/user/_info.html.twig b/templates/user/_info.html.twig
index 72ff6552a..b86d90eda 100644
--- a/templates/user/_info.html.twig
+++ b/templates/user/_info.html.twig
@@ -19,6 +19,7 @@
{% endif %}
From a0eb6cfe5ed55c9bb2728069ab8b4fe3fe53a74f Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Fri, 28 Jun 2024 14:23:11 +0200
Subject: [PATCH 063/335] Translations update from Hosted Weblate (#871)
Co-authored-by: hankskyjames777
---
translations/messages.fil.yaml | 48 ++++++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/translations/messages.fil.yaml b/translations/messages.fil.yaml
index 58753bcad..e03179702 100644
--- a/translations/messages.fil.yaml
+++ b/translations/messages.fil.yaml
@@ -82,3 +82,51 @@ marked_for_deletion: Naka-marka para sa pagbura
marked_for_deletion_at: Naka-marka para sa pagbura sa %date%
enter_your_post: Ipasok ang iyong post
comments_count: '{0}Mga puna|{1}Puna|]1,Inf[ Mga puna'
+notifications: Mga abiso
+blocked: Hinarangan
+show_profile_followings: Ipakita ang mga sinusundan na tagagamit
+expand: Palakihin
+size: Laki
+left: Kaliwa
+right: Kanan
+on: Pinagana
+ban: Bawalan
+rejected: Tinatanggihan
+bans: Mga pagbawal
+created: Ginawa
+pages: Mga pahina
+Your account is not active: Hindi aktibo ang iyong account.
+ban_account: Bawalan ang account
+Your account has been banned: Binabawalan na ang iyong account.
+mercure_enabled: Pinagana ang Mercure
+filter.adult.show: Ipakita ang NSFW
+filter.adult.only: NSFW lamang
+filter.adult.hide: Itago ang NSFW
+filter.fields.names_and_descriptions: Mga pamagat at paglalarawan
+filter.fields.only_names: Mga pamagat lamang
+your_account_has_been_banned: Binabawalan na ang iyong account
+errors.server403.title: 403 Ipinagbabawal
+errors.server500.title: 500 Pangloob na pagkamali sa Serbiro
+block: Harangan
+oauth.consent.allow: Payagan
+oauth2.grant.moderate.magazine_admin.create: Gumawa ng mga bagong magasin.
+oauth2.grant.moderate.magazine_admin.delete: Burahin ang ilan sa mga magasin na pinag-aari
+ mo.
+oauth2.grant.moderate.magazine_admin.all: Gawin, baguhin, o burahin ang mga magasin
+ na pinag-aari mo.
+moderation.report.reject_report_title: Tanggihan ang Ulat
+moderation.report.ban_user_description: Nais mo bang bawalan ang tagagamit (%username%)
+ na gumagawa ng nilalaman na ito mula sa magasin na ito?
+subject_reported_exists: Inuulat na ang nilalaman na ito.
+purge_content: Purgahin ang nilalaman
+moderation.report.approve_report_confirmation: Sigorado ka bang nais mo na aprubahin
+ ang ulat na ito?
+ban_hashtag_btn: Bawalan ang Hashtag
+errors.server429.title: 429 Masyadong Maraming mga Hiling
+errors.server404.title: 404 Hindi nakita
+oauth.consent.to_allow_access: Upang payagan ang access, pindutin ang pindutang 'Payagan'
+ sa ilalim
+moderation.report.approve_report_title: Aprubahin ang Ulat
+moderation.report.ban_user_title: Bawalan ang Tagagamit
+delete_content: Tanggalin ang nilalaman
+oauth.consent.deny: Tanggihan
From c209878ffb04112f11681ac09c43d4288e7841d9 Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Fri, 28 Jun 2024 12:31:37 +0000
Subject: [PATCH 064/335] findActorOrCreate was changed to search by
apPublicUrl which is wrong, revert to apProfileId (#870)
---
migrations/Version20240628142700.php | 26 ++++++++++++++++++++++++++
src/Service/ActivityPubManager.php | 4 ++--
2 files changed, 28 insertions(+), 2 deletions(-)
create mode 100644 migrations/Version20240628142700.php
diff --git a/migrations/Version20240628142700.php b/migrations/Version20240628142700.php
new file mode 100644
index 000000000..5d1c43e93
--- /dev/null
+++ b/migrations/Version20240628142700.php
@@ -0,0 +1,26 @@
+addSql('DROP INDEX user_ap_public_url_idx');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('CREATE UNIQUE INDEX user_ap_public_url_idx ON "user" (ap_public_url)');
+ }
+}
diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php
index 9e3685bb5..76f3a1ddd 100644
--- a/src/Service/ActivityPubManager.php
+++ b/src/Service/ActivityPubManager.php
@@ -157,7 +157,7 @@ public function findActorOrCreate(?string $actorUrlOrHandle): null|User|Magazine
return $this->userRepository->findOneBy(['username' => $name]);
}
- $user = $this->userRepository->findOneBy(['apPublicUrl' => $actorUrl]);
+ $user = $this->userRepository->findOneBy(['apProfileId' => $actorUrl]);
if ($user instanceof User) {
$this->logger->debug('found remote user for url "{url}" in db', ['url' => $actorUrl]);
if ($user->apId && (!$user->apFetchedAt || $user->apFetchedAt->modify('+1 hour') < (new \DateTime()))) {
@@ -166,7 +166,7 @@ public function findActorOrCreate(?string $actorUrlOrHandle): null|User|Magazine
return $user;
}
- $magazine = $this->magazineRepository->findOneBy(['apPublicUrl' => $actorUrl]);
+ $magazine = $this->magazineRepository->findOneBy(['apProfileId' => $actorUrl]);
if ($magazine instanceof Magazine) {
$this->logger->debug('found remote user for url "{url}" in db', ['url' => $actorUrl]);
if (!$magazine->apFetchedAt || $magazine->apFetchedAt->modify('+1 hour') < (new \DateTime())) {
From a5e01de5d2c152410bafe4f74a4b3b2e30ed446c Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Fri, 28 Jun 2024 13:27:09 +0000
Subject: [PATCH 065/335] Fix/actor 404 json error (#872)
---
src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php | 6 ++++++
src/Service/ActivityPub/ApHttpClient.php | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php b/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php
index 1950ace1b..ce66848f3 100644
--- a/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php
+++ b/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php
@@ -83,6 +83,12 @@ public function __invoke(ActivityMessage $message): void
return;
}
+ if (null === $user) {
+ $this->logger->warning('Could not find an actor discarding ActivityMessage {m}', ['m' => $message->payload]);
+
+ return;
+ }
+
$this->handle($payload);
}
diff --git a/src/Service/ActivityPub/ApHttpClient.php b/src/Service/ActivityPub/ApHttpClient.php
index eb08cb6d2..c4e229bc2 100644
--- a/src/Service/ActivityPub/ApHttpClient.php
+++ b/src/Service/ActivityPub/ApHttpClient.php
@@ -198,7 +198,7 @@ function (ItemInterface $item) use ($apProfileId) {
if (404 === $response->getStatusCode()) {
// treat a 404 error the same as a tombstone, since we think there was an actor, but it isn't there anymore
- return $this->tombstoneFactory->create($apProfileId);
+ return json_encode($this->tombstoneFactory->create($apProfileId));
}
// Return the content.
From 6661618f8b8bf2024821ff01f2fd94c343b91ef2 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Fri, 28 Jun 2024 17:27:05 +0200
Subject: [PATCH 066/335] Translations update from Hosted Weblate (#873)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: josé m
---
translations/messages.gl.yaml | 1 +
1 file changed, 1 insertion(+)
create mode 100644 translations/messages.gl.yaml
diff --git a/translations/messages.gl.yaml b/translations/messages.gl.yaml
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/translations/messages.gl.yaml
@@ -0,0 +1 @@
+{}
From 968610c3997a79e799d4a3897ba11f46fccf0e6f Mon Sep 17 00:00:00 2001
From: debounced <35878315+nobodyatroot@users.noreply.github.com>
Date: Sat, 29 Jun 2024 07:30:12 -0500
Subject: [PATCH 067/335] Add new exception to provide descriptive error
(returned false) from `openssl_pkey_get_public` in signature validator (#874)
---
src/Exception/InvalidUserPublicKeyException.php | 17 +++++++++++++++++
.../ActivityPub/Inbox/ActivityHandler.php | 5 +++++
src/Service/ActivityPub/SignatureValidator.php | 9 ++++++++-
3 files changed, 30 insertions(+), 1 deletion(-)
create mode 100644 src/Exception/InvalidUserPublicKeyException.php
diff --git a/src/Exception/InvalidUserPublicKeyException.php b/src/Exception/InvalidUserPublicKeyException.php
new file mode 100644
index 000000000..438aa915c
--- /dev/null
+++ b/src/Exception/InvalidUserPublicKeyException.php
@@ -0,0 +1,17 @@
+apHttpClient->getActivityObject($exception->realOrigin, false);
$this->bus->dispatch(new ActivityMessage($body));
+ return;
+ } catch (InvalidUserPublicKeyException $exception) {
+ $this->logger->warning("Unable to extract public key for '{user}'.", ['user' => $exception->apProfileId]);
+
return;
}
}
diff --git a/src/Service/ActivityPub/SignatureValidator.php b/src/Service/ActivityPub/SignatureValidator.php
index dde46f6fb..84eda3481 100644
--- a/src/Service/ActivityPub/SignatureValidator.php
+++ b/src/Service/ActivityPub/SignatureValidator.php
@@ -6,6 +6,7 @@
use App\Exception\InboxForwardingException;
use App\Exception\InvalidApSignatureException;
+use App\Exception\InvalidUserPublicKeyException;
use App\Message\ActivityPub\Inbox\ActivityMessage;
use App\Service\ActivityPubManager;
use Psr\Log\LoggerInterface;
@@ -31,7 +32,8 @@ public function __construct(
* @param array $headers Headers attached to the incoming request
* @param string $body The body of the incoming request
*
- * @throws InvalidApSignatureException The HTTP request was not signed appropriately
+ * @throws InvalidApSignatureException The HTTP request was not signed appropriately
+ * @throws InvalidUserPublicKeyException The public key of the specified user is invalid or null
* @throws InboxForwardingException
*/
public function validate(array $request, array $headers, string $body): void
@@ -112,6 +114,11 @@ public function validate(array $request, array $headers, string $body): void
$user = $this->activityPubManager->findActorOrCreate($actorUrl);
if (!empty($user)) {
$pkey = openssl_pkey_get_public($this->client->getActorObject($user->apProfileId)['publicKey']['publicKeyPem']);
+
+ if (false === $pkey) {
+ throw new InvalidUserPublicKeyException($user->apProfileId);
+ }
+
$this->verifySignature($pkey, $signature, $headers, $request['uri'], $body);
}
}
From b3240c0d92635fc41bf6777d460c14b1316769df Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Sat, 29 Jun 2024 15:27:29 +0200
Subject: [PATCH 068/335] Translations update from Hosted Weblate (#875)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: hankskyjames777
Co-authored-by: josé m
---
translations/messages.fil.yaml | 20 +
translations/messages.gl.yaml | 735 ++++++++++++++++++++++++++++++++-
2 files changed, 754 insertions(+), 1 deletion(-)
diff --git a/translations/messages.fil.yaml b/translations/messages.fil.yaml
index e03179702..ee2998343 100644
--- a/translations/messages.fil.yaml
+++ b/translations/messages.fil.yaml
@@ -130,3 +130,23 @@ moderation.report.approve_report_title: Aprubahin ang Ulat
moderation.report.ban_user_title: Bawalan ang Tagagamit
delete_content: Tanggalin ang nilalaman
oauth.consent.deny: Tanggihan
+type.magazine: Magasin
+magazine: Magasin
+magazines: Mga magasin
+dont_have_account: Wala bang account?
+you_cant_login: Nakalimutan ang password?
+repeat_password: Ulitin ang password
+all_magazines: Lahat ng mga magasin
+create_new_magazine: Gumawa ng bagong magasin
+change_theme: Palitan ang tema
+select_magazine: Pumili ng magasin
+joined: Sumali noong
+logout: Mag log out
+share_on_fediverse: Ibahagi sa Fediverse
+edit: Baguhin
+are_you_sure: Sigurado ka ba?
+share: Ibahagi
+votes: Mga boto
+yes: Oo
+no: Hindi
+subject_reported: Iniulat na ang nilalaman na ito.
diff --git a/translations/messages.gl.yaml b/translations/messages.gl.yaml
index 0967ef424..cf2da0116 100644
--- a/translations/messages.gl.yaml
+++ b/translations/messages.gl.yaml
@@ -1 +1,734 @@
-{}
+subscribe_for_updates: Subscríbete para comezar a recibir actualizacións.
+ban_hashtag_description: Ao vetar un cancelo non se crearán publicacións con este
+ cancelo, e as publicacións existentes que o conteñan serán agochadas.
+unban: Retirar veto
+ban_hashtag_btn: Vetar Cancelo
+registration_disabled: Non se permite crear novas contas
+restore: Restablecer
+add_mentions_entries: Engadir etiquetas de mención nas conversas
+add_mentions_posts: Engadir etiquetas de mención nas publicacións
+Password is invalid: Contrasinal incorrecto.
+Your account is not active: A conta non está activa.
+Your account has been banned: A túa conta foi vetada.
+firstname: Nome
+send: Enviar
+active_users: Persoas activas
+random_entries: Conversas ao chou
+related_entries: Conversas relacionadas
+purge_account: Purgar conta
+ban_account: Vetar conta
+unban_account: Retirar veto á conta
+related_magazines: Revistas relacionadas
+random_magazines: Revistas ao chou
+sidebar: Barra lateral
+auto_preview: Vista previa automática
+dynamic_lists: Listas dinámicas
+banned_instances: Instancias vetadas
+kbin_intro_title: Explora o Fediverso
+kbin_intro_desc: é unha plataforma descentralizada para contidos agregados e microblogs
+ que actúa dentro da rede Fediverso.
+kbin_promo_title: Crea a túa propia instancia
+kbin_promo_desc: '%link_start%Clona o repositorio%link_end% e espalla o fediverso'
+captcha_enabled: Captcha activado
+header_logo: Logo da cabeceira
+browsing_one_thread: Só estás a ver un dos fíos da conversa! Os comentarios ao completo
+ están dispoñibles na páxina da publicación.
+mercure_enabled: Mercure activado
+report_issue: Denunciar problema
+tokyo_night: Tokyo Night
+sticky_navbar_help: A barra de navegación estará fixa na parte superior da páxina
+ ao desprazarte.
+auto_preview_help: Desprega automáticamente a vista previa do multimedia.
+reload_to_apply: Recarga a páxina para aplicar os cambios
+filter.origin.label: Elixe orixe
+filter.fields.label: Elixe os campos nos que buscar
+filter.adult.label: Elixe se queres mostrar contido NSFW
+filter.adult.hide: Agochar NSFW
+filter.adult.show: Mostrar NSFW
+filter.adult.only: Só NSFW
+local_and_federated: Local e federado
+filter.fields.only_names: Só nomes
+filter.fields.names_and_descriptions: Nomes e descricións
+password_confirm_header: Confirma a solicitude de cambio de contrasinal.
+your_account_is_not_active: A tú conta non foi activada. Mira na caixa de entrada
+ do correo para ver as instruccións para activala ou solicita
+ un novo correo para activala.
+toolbar.strikethrough: Riscada
+toolbar.header: Cabeceira
+toolbar.ordered_list: Lista con orde
+toolbar.mention: Mención
+federation_page_enabled: Páxina de federación activada
+your_account_has_been_banned: Vetouse a túa conta
+toolbar.bold: Grosa
+toolbar.italic: Cursiva
+federation_page_allowed_description: Instancias coñecidas coas que federamos
+federation_page_disallowed_description: Instancias coas que non federamos
+federated_search_only_loggedin: A busca federada está limitada se non inicias sesión
+account_deletion_title: Eliminación da Conta
+more_from_domain: Máis desde o dominio
+errors.server429.title: 429 Demasiadas Solicitudes
+errors.server404.title: 404 Non se atopa
+errors.server403.title: 403 Non autorizado
+email_confirm_button_text: Confirma a túa solicitude de cambio de contrasinal
+email_confirm_link_help: Ou tamén podes copiar e pegar o seguinte no teu navegador
+email.delete.title: Solicitude de eliminación da conta
+email.delete.description: Esta usuaria solicitou que se elimine a súa conta
+resend_account_activation_email_question: Conta inactiva?
+resend_account_activation_email_error: Houbo un problema ao enviar esta solicitude.
+ Pode que non haxa unha conta asociada con este correo ou que xa foi activada.
+resend_account_activation_email_success: Se existe unha conta asociada a este correo,
+ eviaremos un novo correo de activación.
+resend_account_activation_email_description: Escribe o enderezo de correo asociado
+ á conta. Enviaremosche outro correo de activación.
+custom_css: CSS personalizado
+resend_account_activation_email: Reenviar correo de activación da conta
+ignore_magazines_custom_css: Ignorar CSS personalizado das revistas
+oauth.consent.title: Formulario de consentimento OAuth2
+oauth.consent.grant_permissions: Conceder Permisos
+oauth.consent.app_has_permissions: xa pode realizar as seguintes accións
+oauth.consent.to_allow_access: Para permitir este acceso, preme no botón 'Permitir'
+oauth.consent.allow: Permitir
+oauth.consent.deny: Negar
+oauth.client_identifier.invalid: ID de Cliente OAuth non válido!
+oauth.client_not_granted_message_read_permission: Esta app non ten permiso para ler
+ as túas mensaxes.
+restrict_oauth_clients: Restrinxir a creación de Clientes OAuth2 a Admins
+block: Bloquear
+unblock: Desbloquear
+oauth2.grant.moderate.magazine.ban.delete: Retirar veto a usuarias nas revistas que
+ moderas.
+oauth2.grant.moderate.magazine.list: Ler a lista das revistas que moderas.
+oauth2.grant.moderate.magazine.reports.all: Xestionar as denuncias nas revistas que
+ moderas.
+oauth2.grant.moderate.magazine.reports.read: Ler as denuncias nas revistas que moderas.
+oauth2.grant.moderate.magazine.reports.action: Aceptar ou rexeitar denuncias nas revistas
+ que moderas.
+oauth2.grant.moderate.magazine.trash.read: Ver contido eliminado nas revistas que
+ moderas.
+oauth2.grant.moderate.magazine_admin.create: Crear novas revistas.
+oauth2.grant.moderate.magazine_admin.delete: Eliminar calquera das túas propias revistas.
+oauth2.grant.moderate.magazine_admin.update: Editar calquera das regras das túas revistas,
+ descrición, estado NSFW ou icona.
+oauth2.grant.moderate.magazine_admin.edit_theme: Editar o CSS personalizado de calquera
+ das túas revistas.
+oauth2.grant.moderate.magazine_admin.moderators: Engadir ou eliminar moderadoras de
+ calquera das túas revistas.
+oauth2.grant.moderate.magazine_admin.badges: Crear ou eliminar insignias das túas
+ revistas.
+oauth2.grant.moderate.magazine_admin.tags: Crear ou eliminar etiquetas das túas revistas.
+oauth2.grant.moderate.magazine_admin.stats: Ver contido, votar, e ver estatísticas
+ das túas revistas.
+oauth2.grant.admin.all: Realizar tarefas administrativas na túa instancia.
+oauth2.grant.admin.entry.purge: Eliminar completamente unha conversa da túa instancia.
+oauth2.grant.read.general: Ler todo o contido ao que ti teñas acceso.
+oauth2.grant.delete.general: Eliminar calquera conversa, publicación ou comentario.
+oauth2.grant.report.general: Denunciar conversas, publicacións ou comentarios.
+oauth2.grant.vote.general: Voto positivo ou negativo, promocionar conversas, publicacións
+ ou comentarios.
+oauth2.grant.subscribe.general: Subscribirse ou seguir calquera revista, dominio ou
+ usuaria así como ver revistas, dominios e usuarias ás que te subscribiches.
+oauth2.grant.block.general: Bloquear ou desbloquear calquera revista, dominio ou usuaria,
+ así como ver revistas, dominios e usuarias que bloqueaches.
+oauth2.grant.domain.all: Subscribirse ou bloquear dominios, así como ver os dominios
+ aos que te subscribiches ou bloqueaches.
+oauth2.grant.domain.subscribe: Subscribirse ou darse de baixa de dominios e ver os
+ dominios aos que te subscribiches.
+oauth2.grant.domain.block: Bloquear ou desbloquear dominios e ver os dominios que
+ tes bloqueados.
+oauth2.grant.entry.report: Denunciar calquera conversa.
+oauth2.grant.entry_comment.all: Crear, editar ou eliminar os teus comentarios en conversas,
+ e votar, promover ou denunciar calquera comentario nunha conversa.
+oauth2.grant.entry_comment.create: Crear novos comentarios en conversas.
+oauth2.grant.entry_comment.edit: Editar os teus comentarios existentes en conversas.
+oauth2.grant.entry_comment.delete: Eliminar os teus comentarios en conversas.
+oauth2.grant.entry_comment.vote: Voto positivo ou negativo, promoción de calquera
+ comentario nunha conversa.
+oauth2.grant.entry_comment.report: Denunciar calquera comentario nunha conversa.
+oauth2.grant.magazine.block: Bloquear e desbloquear revistas e ver as revistas que
+ tes bloqueadas.
+oauth2.grant.post.all: Crear, editar ou eliminar microblogs, e votar, promover ou
+ denunciar calquera microblog.
+oauth2.grant.post.create: Crear novas publicacións.
+oauth2.grant.post.edit: Editar as túas publicacións.
+oauth2.grant.magazine.subscribe: Subscribir ou dar de baixa dunha revista e ver as
+ revistas ás que te subscribiches.
+oauth2.grant.post.delete: Eliminar as túas publicacións.
+oauth2.grant.post.vote: Voto positivo ou negativo, ou promoción de calquera publicación.
+oauth2.grant.post_comment.delete: Eliminar os teus comentarios nas publicacións.
+oauth2.grant.post_comment.vote: Voto positivo, promoción ou voto negativo en calquera
+ comentario nunha publicación.
+oauth2.grant.user.all: Ler e editar o teu perfil, mensaxes ou notificacións; Ler e
+ editar os permisos concedidos a outras apps; seguir ou bloquear outras usuarias;
+ ver listas de usuarias que segues ou bloqueas.
+oauth2.grant.user.profile.read: Ler o teu perfil.
+oauth2.grant.user.profile.edit: Editar o teu perfil.
+oauth2.grant.user.message.all: Ler as túas mensaxes e enviar mensaxes a outras usuarias.
+oauth2.grant.user.message.read: Ler as túas mensaxes.
+oauth2.grant.user.message.create: Enviar mensaxes a outras usuarias.
+oauth2.grant.post.report: Denunciar calquera publicación.
+oauth2.grant.post_comment.all: Crear, editar ou eliminar os teus comentarios en publicacións,
+ e votar, promover ou denunciar calquera comentario nunha publicación.
+oauth2.grant.user.follow: Seguir e deixar de seguir usuarias, e ler a lista das usuarias
+ que segues.
+oauth2.grant.user.block: Bloquear e desbloquear usuarias, e ler a lista de usuarias
+ que bloqueaches.
+oauth2.grant.moderate.all: Realizar accións de moderación sobre os asuntos que tes
+ permiso nas revistas que moderas.
+oauth2.grant.user.notification.all: Ler e limpar as notificacións.
+oauth2.grant.moderate.entry.all: Moderar conversas nas revistas que moderas.
+oauth2.grant.user.notification.read: Ler as notificacións, incluíndo as notificacións
+ das mensaxes.
+oauth2.grant.user.notification.delete: Limpar as notificacións.
+oauth2.grant.user.oauth_clients.all: Ler e editar os permisos que concedeches a outras
+ aplicacións OAuth2.
+oauth2.grant.user.oauth_clients.read: Ler os permisos que concedeches a outras aplicacións
+ OAuth2.
+oauth2.grant.moderate.entry.set_adult: Marcar as conversas como NSFW nas revistas
+ que moderas.
+oauth2.grant.moderate.entry_comment.all: Moderar comentarios nas conversas nas revistas
+ que moderas.
+oauth2.grant.moderate.entry.trash: Eliminar ou restablecer conversas nas revistas
+ que moderas.
+oauth2.grant.moderate.entry_comment.change_language: Cambiar o idioma dos comentarios
+ nas conversas nas revistas que moderas.
+oauth2.grant.moderate.post.change_language: Cambiar o idioma das publicacións nas
+ revistas que moderas.
+oauth2.grant.moderate.entry_comment.set_adult: Marcar comentarios en conversas como
+ NSFW nas revistas que moderas.
+oauth2.grant.moderate.post.set_adult: Marcar as publicacións como NSFW nas revistas
+ que moderas.
+oauth2.grant.moderate.entry_comment.trash: Eliminar ou restablecer comentarios en
+ conversas nas revistas que moderas.
+oauth2.grant.moderate.post.all: Moderar publicacións nas revistas que moderas.
+oauth2.grant.moderate.post.trash: Eliminar ou restablecer as publicacións nas revistas
+ que moderas.
+oauth2.grant.moderate.post_comment.all: Moderar comentarios nas publicacións das revistas
+ que moderas.
+oauth2.grant.admin.entry_comment.purge: Eliminar completamente un comentario nunha
+ conversa da túa instancia.
+oauth2.grant.moderate.post_comment.change_language: Cambiar os idioma dos comentarios
+ nas publicacións das revistas que moderas.
+oauth2.grant.moderate.post_comment.set_adult: Marcar comentarios como NSFW nas publicacións
+ das revistas que moderas.
+oauth2.grant.admin.post.purge: Eliminar completamente calquera publicación da túa
+ instancia.
+oauth2.grant.admin.post_comment.purge: Eliminar completamente calquera comentario
+ nunha publicación da túa instancia.
+oauth2.grant.admin.magazine.all: Mover de lugar as conversas ou eliminar completamente
+ revistas da túa instancia.
+oauth2.grant.moderate.post_comment.trash: Eliminar ou restablecer comentarios nas
+ publicacións das revistas que moderas.
+oauth2.grant.moderate.magazine.all: Xestionar vetos, denuncias e ver elementos eliminados
+ nas revistas que moderas.
+oauth2.grant.moderate.magazine.ban.all: Xestionar usuarias vetadas nas revistas que
+ moderas.
+oauth2.grant.moderate.magazine.ban.read: Ver as usuarias vetadas nas revistas que
+ moderas.
+oauth2.grant.moderate.magazine.ban.create: Vetar usuarias nas revistas que moderas.
+oauth2.grant.admin.magazine.move_entry: Mover conversas entre revistas na túa instancia.
+oauth2.grant.admin.instance.settings.edit: Actualizar os axustes da túa instancia.
+oauth2.grant.admin.magazine.purge: Eliminar completamente revistas da túa instancia.
+oauth2.grant.admin.user.all: Vetar, verificar ou eliminar completamente usuarias da
+ túa instancia.
+oauth2.grant.admin.instance.information.edit: Actualizar o Acerca de, PMF, Contacto,
+ Termos do Servizo e Política de Privacidade da túa instancia.
+oauth2.grant.admin.federation.all: Ver e actualizar as instancias actualmente desfederadas.
+oauth2.grant.admin.user.ban: Vetar ou restablecer usuarias da túa instancia.
+oauth2.grant.admin.user.verify: Verificar usuarias da túa instancia.
+oauth2.grant.admin.user.delete: Eliminar usuarias da túa instancia.
+oauth2.grant.admin.user.purge: Eliminar completamente usuarias da túa instancia.
+oauth2.grant.admin.instance.all: Ver e actualizar os axustes da instancia ou a información.
+oauth2.grant.admin.instance.stats: Ver estatísticas da túa instancia.
+oauth2.grant.admin.instance.settings.all: Ver ou actualizar os axustes da túa instancia.
+oauth2.grant.admin.instance.settings.read: Ver os axustes da túa instancia.
+oauth2.grant.admin.federation.read: Ver a lista das instancias desfederadas.
+oauth2.grant.admin.federation.update: Engadir ou eliminar instancias da lista de instancias
+ desfederadas.
+oauth2.grant.admin.oauth_clients.all: Ver ou revogar clientes OAuth2 que existan na
+ túa instancia.
+oauth2.grant.admin.oauth_clients.read: Ver os clientes OAuth2 existentes na túa instancia,
+ e as súas estatísticas de uso.
+oauth2.grant.admin.oauth_clients.revoke: Revogar o acceso a clientes OAuth2 na túa
+ instancia.
+last_active: Última actividade
+flash_post_pin_success: Fixouse correctamente a publicación.
+flash_post_unpin_success: Soltouse correctamente a publicación.
+comment_reply_position_help: Mostar a resposta ao comentario ou ben arriba ou embaixo
+ na páxina. Se activas o 'desprazamento infinito' a posición sempre será arriba.
+show_avatars_on_comments: Mostrar avatares nos comentarios
+single_settings: Único
+comment_reply_position: Posición do comentario de resposta
+magazine_theme_appearance_custom_css: CSS personalizado que se aplicará ao ver o contido
+ na túa revista.
+magazine_theme_appearance_icon: Icona personalizada para a revista. Se non escolles
+ ningún, usarase a icona por defecto.
+magazine_theme_appearance_background_image: Imaxe de fondo personalizada que se aplicará
+ ao ver o contido na túa revista.
+delete_content_desc: Eliminar o contido da usuaria pero deixar nas conversas, publicacións
+ e comentarios as respostas doutras usuarias.
+purge_content_desc: Purgar completamente o contido da usuaria, incluindo as respostas
+ doutras usuarias nas conversas creadas, publicacións e comentarios.
+two_factor_authentication: Autenticación con dous factores
+two_factor_backup: Códigos de apoio do segundo factor de autenticación
+2fa.authentication_code.label: Código de Autenticación
+2fa.verify: Verificar
+2fa.code_invalid: O código de autenticación non é válido
+moderation.report.approve_report_title: Aprobar Denuncia
+moderation.report.reject_report_confirmation: Tes a certeza de querer rexeitar esta
+ denuncia?
+oauth2.grant.moderate.post.pin: Fixar publicacións na parte superior das revistas
+ que moderas.
+2fa.enable: Configurar o segundo factor de autenticación
+2fa.disable: Desactivar o segundo factor de autenticación
+2fa.backup-create.label: Crear novos códigos de autenticación de apoio
+2fa.add: Engadir á conta
+2fa.verify_authentication_code.label: Escribe o código do segundo factor para verificar
+2fa.backup: Códigos de apoio do segundo factor
+2fa.backup-create.help: Podes crear novos códigos de apoio para a autenticación; ao
+ facelo invalidarás os existentes.
+2fa.qr_code_img.alt: Un código QR que configura o segundo factor de autenticación
+ para a túa conta
+2fa.qr_code_link.title: Ao visitar esta ligazón permitirás á túa aplicación rexistrar
+ este segundo elemento de autenticación
+2fa.available_apps: Usar unha app tal que %google_authenticator%, %aegis% (Android)
+ ou %raivo% (iOS) como segundo factor para escanear o código QR.
+2fa.backup_codes.help: Podes usar estes códigs cando non tes a man a app ou dispositivo
+ de segundo factor. Non volverán a mostrarse e ademáis só se pode
+ usar unha única vez cada un.
+2fa.backup_codes.recommendation: Recomendamos que gardes unha copia dos códigos nun
+ lugar seguro.
+cancel: Cancelar
+account_settings_changed: Cambiouse correctamente a configuración da conta. Deberás
+ iniciar sesión outra vez.
+magazine_deletion: Eliminación da Revista
+delete_magazine: Eliminar revista
+restore_magazine: Restablecer revista
+purge_magazine: Purgar revista
+magazine_is_deleted: Eliminouse a revista. Podes restablecela
+ durante os seguintes 30 días.
+user_suspend_desc: Ao suspender a túa conta agochar o seu contido na instancia, pero
+ non a eliminar de xeito permanente, podes restablecela cando queiras.
+deletion: Eliminación
+remove_subscriptions: Retirar as subscricións
+apply_for_moderator: Solicita axudar coa moderación
+request_magazine_ownership: Solicita a propiedade da revista
+cancel_request: Retirar a solicitude
+ownership_requests: Solicitudes de propiedade
+accept: Aceptar
+moderator_requests: Solicitudes de Mod
+open_url_to_fediverse: Abrir URL orixinal
+marked_for_deletion: Marcado para eliminación
+magazines: Revistas
+search: Buscar
+add: Engadir
+login: Acceder
+sort_by: Orde por
+filter_by_subscription: Filtrar por subscrición
+filter_by_federation: Filtrar por estado da federación
+posts: Publicacións
+replies: Respostas
+moderators: Moderación
+mod_log: Rexistro da moderación
+add_comment: Engadir comentario
+add_post: Engadir publicación
+add_media: Engadir multimedia
+remove_media: Retirar multimedia
+markdown_howto: Como funciona o editor?
+enter_your_comment: Escribe o comentario
+enter_your_post: Escribe a publicación
+activity: Actividade
+always_disconnected_magazine_info: Esta revista non recibe actualizacións.
+go_to_original_instance: Ver nunha instancia remota
+from: desde
+change_theme: Cambiar decorado
+useful: Útil
+help: Axuda
+check_email: Comproba o correo
+reset_check_email_desc: Se xa existe unha conta asociada ao teu enderezo de correo
+ electrónico, axiña recibirás un correo cunha ligazón para restablecer o contrasinal.
+ A ligazón caducará en %expire%.
+reset_check_email_desc2: Se non recibes o correo electrónico, mira no cartafol de
+ spam.
+try_again: Volve a intentalo
+up_vote: Promover
+down_vote: Reducir
+email_confirm_content: 'Activamos a túa conta Mbin? Preme na ligazón inferior:'
+tag: Etiqueta
+columns: Columnas
+user: Usuaria
+joined: Alta
+moderated: Moderada
+people_local: Local
+people_federated: Federada
+copy_url: Copiar URL Mbin
+settings: Axustes
+general: Xeral
+profile: Perfil
+menu: Menú
+privacy: Privacidade
+default_theme: Decorado por defecto
+default_theme_auto: Claro/Escuro (Auto)
+solarized_auto: Solarizado (Auto)
+flash_magazine_edit_success: Editouse correctamente a revista.
+flash_mark_as_adult_success: Publicación marcada correctamente como NSFW.
+flash_unmark_as_adult_success: Retirouse correctamente a marca de NSFW.
+too_many_requests: Excedeches o límite, inténtao outra vez máis tarde.
+right: Dereita
+federation: Federación
+status: Estado
+on: On
+off: Off
+instances: Instancias
+upload_file: Subir ficheiro
+from_url: Desde url
+reject: Rexeitar
+unban_hashtag_btn: Retirar veto ao Cancelo
+unban_hashtag_description: Ao retirarlle o veto ao cancelo permites a creación de
+ publicacións con ese cancelo. As publicacións existentes co cancelo volverán ser
+ visibles.
+filters: Filtros
+approved: Aprobado
+rejected: Rexeitado
+add_moderator: Engadir moderadora
+add_badge: Engadir insignia
+bans: Vetos
+created: Creado
+icon: Icona
+done: Feito
+pin: Fixar
+unpin: Soltar
+change: Cambiar
+mark_as_adult: Marcar como NSFW
+unmark_as_adult: Desmarcar como NSFW
+pinned: Fixado
+preview: Vista previa
+article: Conversa
+reputation: Reputación
+note: Nota
+writing: Ao escribir
+users: Usuarias
+content: Contido
+dashboard: Taboleiro
+contact_email: Email de contacto
+meta: Meta
+instance: Instancia
+delete_account: Eliminar conta
+magazine_panel_tags_info: Escribe algo só se queres que se inclúa nesta revista contido
+ do fediverso en función das etiquetas
+return: Volver
+boost: Promover
+preferred_languages: Filtrar as conversas e publicacións por idioma
+infinite_scroll_help: Cargar automáticamente máis contido cando acadas o fin da páxina.
+kbin_bot: Mbin Agent
+toolbar.quote: Cita
+toolbar.code: Código
+toolbar.link: Ligazón
+toolbar.image: Imaxe
+toolbar.unordered_list: Lista sen orde
+oauth.consent.app_requesting_permissions: quere realizar as seguintes accións no teu
+ nome
+oauth2.grant.moderate.magazine_admin.all: Crear, editar ou eliminar as túas propias
+ revistas.
+oauth2.grant.write.general: Crear ou editar todas as túas conversas, publicacións
+ ou comentarios.
+oauth2.grant.entry.all: Crear, editar ou eliminar conversas, e votar, promover ou
+ denunciar calquera conversa.
+oauth2.grant.entry.create: Crear novas conversas.
+oauth2.grant.entry.edit: Editar as conversas existentes.
+oauth2.grant.entry.delete: Eliminar as túas conversas existentes.
+oauth2.grant.entry.vote: Voto positivo ou negativo e promocion de calquera conversa.
+oauth2.grant.magazine.all: Subscribirse ou bloquear revistas, así como ver as revistas
+ ás que te subscribiches ou bloqueaches.
+oauth2.grant.post_comment.create: Crear novos comentarios en publicacións.
+oauth2.grant.post_comment.edit: Editar os teus comentarios nas publicacións.
+oauth2.grant.post_comment.report: Denunciar calquera comentario nunha publicación.
+oauth2.grant.user.profile.all: Ler e editar o teu perfil.
+oauth2.grant.user.oauth_clients.edit: Editar os permisos que concedeches a outras
+ aplicacións OAuth2.
+oauth2.grant.moderate.entry.change_language: Cambiar o idioma das conversas nas revistas
+ que moderas.
+oauth2.grant.moderate.entry.pin: Fixar conversas nas revistas que moderas.
+update_comment: Actualizar comentario
+show_avatars_on_comments_help: Mostra/Agocha os avatares ao ver os comentarios nunha
+ conversa única ou publicación.
+moderation.report.reject_report_title: Rexeitar Denuncia
+moderation.report.ban_user_description: Queres vetar a usuaria (%username%) que creou
+ este contido nesta revista?
+moderation.report.approve_report_confirmation: Tes a certeza de querer aprobar esta
+ denuncia?
+subject_reported_exists: Este contido xa foi denunciado.
+moderation.report.ban_user_title: Vetar Usuaria
+delete_content: Eliminar contido
+purge_content: Purgar contido
+2fa.remove: Desbotar 2FA
+pending: Pendente
+suspend_account: Suspender conta
+account_suspended: A conta foi suspendida.
+remove_following: Retirar o seguimento
+abandoned: Abandonado
+top: votos
+type.link: Ligazón
+type.article: Conversa
+type.photo: Foto
+type.video: Vídeo
+type.smart_contract: Pregado intelixente
+type.magazine: Revista
+thread: Conversa
+threads: Conversas
+microblog: Microblog
+people: Persoas
+events: Eventos
+magazine: Revista
+select_channel: Escolle unha canle
+hot: En voga
+active: Activo
+newest: Máis novo
+oldest: Máis antigo
+commented: Comentado
+change_view: Cambiar a vista
+filter_by_time: Filtrar por data
+avatar: Avatar
+added: Engadido
+up_votes: Promocións
+down_votes: Reprobar
+no_comments: Sen comentarios
+created_at: Creado
+filter_by_type: Filtrar por tipo
+favourites: Favoritas
+favourite: Favorecer
+more: Máis
+owner: Creadora
+subscribers: Subscritoras
+online: Con conexión
+comments: Comentarios
+cover: Portada
+related_posts: Publicacións relacionadas
+random_posts: Publicacións ao chou
+federated_magazine_info: Esta revista procede dun servidor federado e podería non
+ estar completa.
+empty: Baleiro
+subscribe: Subscríbete
+unsubscribe: Retira a subscrición
+federated_user_info: Este perfil procede dun servidor federado e podería non estar
+ completo.
+follow: Segue
+unfollow: Retira o seguimento
+reply: Responde
+login_or_email: Identificador ou email
+password: Contrasinal
+remember_me: Lémbrame
+register: Crear conta
+dont_have_account: Non tes unha conta?
+you_cant_login: Esqueceches o contrasinal?
+already_have_account: Xa tes unha conta?
+reset_password: Restablecer contrasinal
+show_more: Saber máis
+to: para
+in: en
+email: Correo electrónico
+username: Identificador
+repeat_password: Repite o contrasinal
+agree_terms: Acepta os %terms_link_start%Termos e Condicións%terms_link_end% así como
+ a %policy_link_start%Política de Privacidade%policy_link_end%
+terms: Termos do servizo
+privacy_policy: Política de privacidade
+fediverse: Fediverso
+create_new_magazine: Crear unha nova revista
+add_new_article: Engadir nova conversa
+about_instance: Acerca de
+all_magazines: Todas as revistas
+stats: Estatísticas
+add_new_link: Engadir nova ligazón
+add_new_photo: Engadir nova foto
+add_new_post: Engadir nova publicación
+add_new_video: Engadir novo vídeo
+contact: Contacto
+faq: PMF
+rss: RSS
+email_confirm_header: Ola! Confirma o teu enderezo de correo.
+email_verify: Confirma o enderezo de correo
+email_confirm_expire: Ten en conta que a ligazón caducará dentro dunha hora.
+email_confirm_title: Confirma o teu enderezo de correo.
+select_magazine: Escolle unha revista
+add_new: Engadir nova
+url: URL
+title: Título
+tags: Etiquetas
+badges: Insignias
+is_adult: 18+ / NSFW
+body: Corpo
+eng: ENG
+oc: OC
+image: Imaxe
+image_alt: Texto alternativo á imaxe
+name: Nome
+description: Descrición
+rules: Regras
+domain: Dominio
+followers: Seguidoras
+following: Seguimentos
+subscriptions: Subscricións
+overview: Vista xeral
+cards: Tarxetas
+reputation_points: Puntos de reputación
+related_tags: Etiquetas relacionadas
+go_to_content: Ir ao contido
+go_to_filters: Ir aos filtros
+go_to_search: Ir á busca
+subscribed: Subscrita
+all: Todo
+logout: Pechar sesión
+chat_view: Vista de conversa
+tree_view: Vista en árbore
+table_view: Vista en táboa
+cards_view: Vista en tarxetas
+classic_view: Vista clásica
+compact_view: Vista compacta
+3h: 3h
+6h: 6h
+12h: 12h
+1d: 1d
+1w: 1s
+1m: 1m
+1y: 1a
+links: Ligazóns
+articles: Conversas
+photos: Fotos
+videos: Vídeos
+report: Denuncia
+share: Comparte
+copy_url_to_fediverse: Copiar URL orixinal
+share_on_fediverse: Comparte no Fediverso
+edit: Editar
+are_you_sure: Tes a certeza?
+edit_article: Editar conversa
+edit_photo: Editar foto
+delete: Eliminar
+edit_post: Editar publicación
+edit_comment: Gardar cambios
+moderate: Moderar
+reason: Razón
+edit_link: Editar ligazón
+blocked: Bloqueado
+reports: Denuncias
+notifications: Notificacións
+messages: Mensaxes
+appearance: Aparencia
+homepage: Páxina de inicio
+hide_adult: Agochar contido NSFW
+featured_magazines: Revistas destacadas
+show_profile_subscriptions: Mostrar subscricións a revistas
+show_profile_followings: Mostrar usuarias seguidas
+notify_on_new_entry_reply: Todos os niveis nas conversas que comecei
+notify_on_new_entry_comment_reply: Respostas a comentarios en calquera conversa
+notify_on_new_post_reply: Respostas de todo nivel ás miñas publicacións
+notify_on_new_post_comment_reply: Respostas aos meus comentarios en calquera publicación
+notify_on_new_entry: Novas conversas (ligazóns ou artigos) nunha revista subscrita
+about: Acerca de
+old_email: Correo actual
+new_email: Novo correo electrónico
+notify_on_new_posts: Novas publicacións en calquera revista á que estou subscrita
+new_email_repeat: Confirmar o novo enderezo
+save: Gardar
+current_password: Contrasinal actual
+new_password: Novo contrasinal
+new_password_repeat: Confirmar o novo contrasinal
+change_email: Cambiar correo electrónico
+change_password: Cambiar contrasinal
+expand: Despregar
+domains: Dominios
+collapse: Pregar
+error: Erro
+votes: Votos
+theme: Decorado
+dark: Escuro
+light: Claro
+solarized_light: Claro solarizado
+solarized_dark: Escuro solarizado
+font_size: Tamaño da letra
+size: Tamaño
+boosts: Promocións
+show_users_avatars: Mostrar avateres das usuarias
+yes: Si
+no: Non
+show_magazines_icons: Mostrar iconas das revistas
+show_thumbnails: Mostrar miniaturas
+rounded_edges: Bordo redondeado
+removed_thread_by: eliminou unha conversa de
+restored_thread_by: restableceu unha conversa de
+removed_comment_by: eliminou un comentario de
+restored_comment_by: restableceu o comentario de
+removed_post_by: eliminou unha publicación de
+restored_post_by: restableceu unha publicación de
+he_banned: vetar
+he_unbanned: retirar veto
+read_all: Ler todo
+show_all: Mostrar todo
+flash_register_success: Benvida! Creouse a túa conta. Para rematar - comproba no correo
+ electrónico se recibiches a ligazón para activar a túa conta.
+flash_thread_new_success: A conversa creouse correctamente e xa é visible para outras
+ usuarias.
+set_magazines_bar: Barra das revistas
+flash_thread_edit_success: Editouse correctamente a conversa.
+flash_thread_delete_success: Eliminouse correctamente a conversa.
+flash_thread_pin_success: A conversa quedou fixada correctamente.
+flash_thread_unpin_success: Soltouse correctamente a conversa.
+flash_magazine_new_success: Creouse correctamente a revista. Xa podes engadir contido
+ ou explorar o panel de administración da revista.
+set_magazines_bar_desc: engade os nomes das revistas após a vírgula
+set_magazines_bar_empty_desc: se o campo queda baleiro, as revistas activas mostraranse
+ na barra.
+mod_log_alert: AVISO - O rexistro da moderación podería conter contido desagradable
+ ou estresante que foi eliminado pola moderación. Procede con cautela.
+added_new_thread: Engadiu nova conversa
+edited_thread: Editou unha conversa
+mod_remove_your_thread: A moderación eliminou a túa conversa
+added_new_comment: Engadiu un novo comentario
+edited_comment: Editou un comentario
+replied_to_your_comment: Respondeu ao teu comentario
+mod_deleted_your_comment: A moderación eliminou o teu comentario
+added_new_post: Engadiu unha nova publicación
+edited_post: Editou unha publicación
+mod_remove_your_post: A moderación eliminou a túa publicación
+added_new_reply: Engadiu unha nova resposta
+wrote_message: Escribeu unha mensaxe
+banned: Vetoute
+removed: Eliminado pola moderación
+deleted: Eliminado pola autora
+mentioned_you: Mencionoute
+comment: Comentar
+post: Publicación
+ban_expired: Caducou o veto
+infinite_scroll: Desprazamento sen fin
+purge: Purgar
+send_message: Enviar mensaxe
+message: Mensaxe
+show_top_bar: Mostrar barra superior
+sticky_navbar: Barra nav. pegañenta
+subject_reported: O contido foi denunciado.
+sidebar_position: Posición da barra lateral
+left: Esquerda
+magazine_panel: Panel da revista
+approve: Aprobar
+expired_at: Caducou o
+ban: Vetar
+expires: Caduca
+perm: Permanente
+add_ban: Engadir veto
+trash: Lixo
+change_magazine: Cambiar revista
+change_language: Cambiar idioma
+week: Semana
+months: Meses
+year: Ano
+federated: Federado
+weeks: Semanas
+month: Mes
+local: Local
+admin_panel: Panel Admin
+pages: Páxinas
+FAQ: PMF
+inactive: Inactivo
+type_search_term: Escribe termo a buscar
+federation_enabled: A federación está activada
+registrations_enabled: Permítese a creación de contas
From 4e43b063ceb88f7b2763cb06dfd0d9fb4ca6e229 Mon Sep 17 00:00:00 2001
From: BentiGorlich
Date: Sat, 29 Jun 2024 13:30:03 +0000
Subject: [PATCH 069/335] Add peertube support (#782)
Co-authored-by: debounced <35878315+nobodyatroot@users.noreply.github.com>
---
migrations/Version20240614120443.php | 48 +++++++++++
src/DTO/EntryCommentDto.php | 3 +
src/DTO/EntryDto.php | 3 +
src/DTO/PostCommentDto.php | 3 +
src/DTO/PostDto.php | 3 +
src/Entity/Entry.php | 2 +-
src/Entity/Traits/VotableTrait.php | 16 +++-
src/Factory/EntryCommentFactory.php | 4 +
src/Factory/EntryFactory.php | 3 +
src/Factory/PostCommentFactory.php | 4 +
src/Factory/PostFactory.php | 3 +
.../ActivityPub/Inbox/ActivityHandler.php | 5 +-
.../Inbox/ChainActivityHandler.php | 3 +-
.../ActivityPub/Inbox/CreateHandler.php | 14 +--
.../ActivityPub/Inbox/UpdateHandler.php | 59 ++++++-------
src/Service/ActivityPub/ApHttpClient.php | 2 +-
src/Service/ActivityPub/Note.php | 28 +++---
src/Service/ActivityPub/Page.php | 14 +--
src/Service/ActivityPubManager.php | 85 ++++++++++++++++++-
src/Service/EntryCommentManager.php | 12 +++
src/Service/EntryManager.php | 12 +++
src/Service/FavouriteManager.php | 21 +++++
src/Service/PostCommentManager.php | 11 +++
src/Service/PostManager.php | 12 +++
src/Service/VoteManager.php | 42 +++++++++
templates/components/boost.html.twig | 6 +-
templates/components/vote.html.twig | 4 +-
templates/entry/_options_activity.html.twig | 2 +-
28 files changed, 343 insertions(+), 81 deletions(-)
create mode 100644 migrations/Version20240614120443.php
diff --git a/migrations/Version20240614120443.php b/migrations/Version20240614120443.php
new file mode 100644
index 000000000..1e5e850cc
--- /dev/null
+++ b/migrations/Version20240614120443.php
@@ -0,0 +1,48 @@
+addSql('ALTER TABLE entry ADD ap_like_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE entry ADD ap_dislike_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE entry ADD ap_share_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE entry_comment ADD ap_like_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE entry_comment ADD ap_dislike_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE entry_comment ADD ap_share_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE post ADD ap_like_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE post ADD ap_dislike_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE post ADD ap_share_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE post_comment ADD ap_like_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE post_comment ADD ap_dislike_count INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE post_comment ADD ap_share_count INT DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('ALTER TABLE entry_comment DROP ap_like_count');
+ $this->addSql('ALTER TABLE entry_comment DROP ap_dislike_count');
+ $this->addSql('ALTER TABLE entry_comment DROP ap_share_count');
+ $this->addSql('ALTER TABLE post_comment DROP ap_like_count');
+ $this->addSql('ALTER TABLE post_comment DROP ap_dislike_count');
+ $this->addSql('ALTER TABLE post_comment DROP ap_share_count');
+ $this->addSql('ALTER TABLE post DROP ap_like_count');
+ $this->addSql('ALTER TABLE post DROP ap_dislike_count');
+ $this->addSql('ALTER TABLE post DROP ap_share_count');
+ $this->addSql('ALTER TABLE entry DROP ap_like_count');
+ $this->addSql('ALTER TABLE entry DROP ap_dislike_count');
+ $this->addSql('ALTER TABLE entry DROP ap_share_count');
+ }
+}
diff --git a/src/DTO/EntryCommentDto.php b/src/DTO/EntryCommentDto.php
index 787fec59d..e21cc9b9c 100644
--- a/src/DTO/EntryCommentDto.php
+++ b/src/DTO/EntryCommentDto.php
@@ -38,6 +38,9 @@ class EntryCommentDto
public ?string $visibility = VisibilityInterface::VISIBILITY_VISIBLE;
public ?string $ip = null;
public ?string $apId = null;
+ public ?int $apLikeCount = null;
+ public ?int $apDislikeCount = null;
+ public ?int $apShareCount = null;
public ?array $mentions = null;
public ?\DateTimeImmutable $createdAt = null;
public ?\DateTimeImmutable $editedAt = null;
diff --git a/src/DTO/EntryDto.php b/src/DTO/EntryDto.php
index 9c2d4197b..fb8eff5cc 100644
--- a/src/DTO/EntryDto.php
+++ b/src/DTO/EntryDto.php
@@ -47,6 +47,9 @@ class EntryDto implements ContentVisibilityInterface
public ?string $visibility = VisibilityInterface::VISIBILITY_VISIBLE;
public ?string $ip = null;
public ?string $apId = null;
+ public ?int $apLikeCount = null;
+ public ?int $apDislikeCount = null;
+ public ?int $apShareCount = null;
public ?array $tags = null;
public ?\DateTimeImmutable $createdAt = null;
public ?\DateTimeImmutable $editedAt = null;
diff --git a/src/DTO/PostCommentDto.php b/src/DTO/PostCommentDto.php
index 9061e4496..0d8073123 100644
--- a/src/DTO/PostCommentDto.php
+++ b/src/DTO/PostCommentDto.php
@@ -38,6 +38,9 @@ class PostCommentDto implements ContentVisibilityInterface
public ?string $visibility = VisibilityInterface::VISIBILITY_VISIBLE;
public ?string $ip = null;
public ?string $apId = null;
+ public ?int $apLikeCount = null;
+ public ?int $apDislikeCount = null;
+ public ?int $apShareCount = null;
public ?array $mentions = null;
public ?\DateTimeImmutable $createdAt = null;
public ?\DateTimeImmutable $editedAt = null;
diff --git a/src/DTO/PostDto.php b/src/DTO/PostDto.php
index 439541507..efdb60f6c 100644
--- a/src/DTO/PostDto.php
+++ b/src/DTO/PostDto.php
@@ -38,6 +38,9 @@ class PostDto implements ContentVisibilityInterface
public ?string $ip = null;
public ?array $mentions = null;
public ?string $apId = null;
+ public ?int $apLikeCount = null;
+ public ?int $apDislikeCount = null;
+ public ?int $apShareCount = null;
public ?\DateTimeImmutable $createdAt = null;
public ?\DateTimeImmutable $editedAt = null;
public ?\DateTime $lastActive = null;
diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php
index 852d2464a..65258f0a1 100644
--- a/src/Entity/Entry.php
+++ b/src/Entity/Entry.php
@@ -283,7 +283,7 @@ public function addVote(Vote $vote): self
public function updateScore(): self
{
- $this->score = $this->getUpVotes()->count() + $this->favouriteCount - $this->getDownVotes()->count();
+ $this->score = ($this->apShareCount ?? $this->getUpVotes()->count()) + ($this->apLikeCount ?? $this->favouriteCount) - ($this->apDislikeCount ?? $this->getDownVotes()->count());
return $this;
}
diff --git a/src/Entity/Traits/VotableTrait.php b/src/Entity/Traits/VotableTrait.php
index 7dfe43b51..dea79c86b 100644
--- a/src/Entity/Traits/VotableTrait.php
+++ b/src/Entity/Traits/VotableTrait.php
@@ -10,6 +10,7 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
+use Doctrine\ORM\Mapping\Column;
trait VotableTrait
{
@@ -19,19 +20,28 @@ trait VotableTrait
#[ORM\Column(type: 'integer')]
private int $downVotes = 0;
+ #[Column(type: 'integer', nullable: true)]
+ public ?int $apLikeCount = null;
+
+ #[Column(type: 'integer', nullable: true)]
+ public ?int $apDislikeCount = null;
+
+ #[Column(type: 'integer', nullable: true)]
+ public ?int $apShareCount = null;
+
public function countUpVotes(): int
{
- return $this->upVotes;
+ return $this->apShareCount ?? $this->upVotes;
}
public function countDownVotes(): int
{
- return $this->downVotes;
+ return $this->apDislikeCount ?? $this->downVotes;
}
public function countVotes(): int
{
- return $this->downVotes + $this->upVotes;
+ return $this->countDownVotes() + $this->countUpVotes();
}
public function getUserChoice(User $user): int
diff --git a/src/Factory/EntryCommentFactory.php b/src/Factory/EntryCommentFactory.php
index c8d7a66fb..79b0a0add 100644
--- a/src/Factory/EntryCommentFactory.php
+++ b/src/Factory/EntryCommentFactory.php
@@ -101,6 +101,10 @@ public function createDto(EntryComment $comment): EntryCommentDto
$dto->createdAt = $comment->createdAt;
$dto->editedAt = $comment->editedAt;
$dto->lastActive = $comment->lastActive;
+ $dto->apId = $comment->apId;
+ $dto->apLikeCount = $comment->apLikeCount;
+ $dto->apDislikeCount = $comment->apDislikeCount;
+ $dto->apShareCount = $comment->apShareCount;
$dto->setId($comment->getId());
$currentUser = $this->security->getUser();
diff --git a/src/Factory/EntryFactory.php b/src/Factory/EntryFactory.php
index b88e9dc8a..b6c1ae1c6 100644
--- a/src/Factory/EntryFactory.php
+++ b/src/Factory/EntryFactory.php
@@ -104,6 +104,9 @@ public function createDto(Entry $entry): EntryDto
$dto->isPinned = $entry->sticky;
$dto->type = $entry->type;
$dto->apId = $entry->apId;
+ $dto->apLikeCount = $entry->apLikeCount;
+ $dto->apDislikeCount = $entry->apDislikeCount;
+ $dto->apShareCount = $entry->apShareCount;
$dto->tags = $this->tagLinkRepository->getTagsOfEntry($entry);
$currentUser = $this->security->getUser();
diff --git a/src/Factory/PostCommentFactory.php b/src/Factory/PostCommentFactory.php
index 81bc0bc8b..c58cc38bb 100644
--- a/src/Factory/PostCommentFactory.php
+++ b/src/Factory/PostCommentFactory.php
@@ -103,6 +103,10 @@ public function createDto(PostComment $comment): PostCommentDto
$dto->setId($comment->getId());
$dto->parent = $comment->parent;
$dto->mentions = $comment->mentions;
+ $dto->apId = $comment->apId;
+ $dto->apLikeCount = $comment->apLikeCount;
+ $dto->apDislikeCount = $comment->apDislikeCount;
+ $dto->apShareCount = $comment->apShareCount;
$currentUser = $this->security->getUser();
// Only return the user's vote if permission to control voting has been given
diff --git a/src/Factory/PostFactory.php b/src/Factory/PostFactory.php
index 6b189c8f5..894853333 100644
--- a/src/Factory/PostFactory.php
+++ b/src/Factory/PostFactory.php
@@ -82,6 +82,9 @@ public function createDto(Post $post): PostDto
$dto->ip = $post->ip;
$dto->mentions = $post->mentions;
$dto->apId = $post->apId;
+ $dto->apLikeCount = $post->apLikeCount;
+ $dto->apDislikeCount = $post->apDislikeCount;
+ $dto->apShareCount = $post->apShareCount;
$dto->setId($post->getId());
$currentUser = $this->security->getUser();
diff --git a/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php b/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php
index 4848f9d1e..719bdeffc 100644
--- a/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php
+++ b/src/MessageHandler/ActivityPub/Inbox/ActivityHandler.php
@@ -68,10 +68,10 @@ public function __invoke(ActivityMessage $message): void
try {
if (isset($payload['actor']) || isset($payload['attributedTo'])) {
- if (!$this->verifyInstanceDomain($payload['actor'] ?? $payload['attributedTo'])) {
+ if (!$this->verifyInstanceDomain($payload['actor'] ?? $this->manager->getActorFromAttributedTo($payload['attributedTo']))) {
return;
}
- $user = $this->manager->findActorOrCreate($payload['actor'] ?? $payload['attributedTo']);
+ $user = $this->manager->findActorOrCreate($payload['actor'] ?? $this->manager->getActorFromAttributedTo($payload['attributedTo']));
} else {
if (!$this->verifyInstanceDomain($payload['id'])) {
return;
@@ -139,6 +139,7 @@ private function handle(?array $payload)
case 'Page':
case 'Article':
case 'Question':
+ case 'Video':
$this->bus->dispatch(new CreateMessage($payload));
// no break
case 'Announce':
diff --git a/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php b/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php
index 13eca0413..ceed0d8ae 100644
--- a/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php
+++ b/src/MessageHandler/ActivityPub/Inbox/ChainActivityHandler.php
@@ -41,7 +41,7 @@ public function __invoke(ChainActivityMessage $message): void
if (!$message->chain || 0 === \sizeof($message->chain)) {
return;
}
- $validObjectTypes = ['Page', 'Note', 'Article', 'Question'];
+ $validObjectTypes = ['Page', 'Note', 'Article', 'Question', 'Video'];
$object = $message->chain[0];
if (!\in_array($object['type'], $validObjectTypes)) {
$this->logger->error('cannot get the dependencies of the object, its type {t} is not one we can handle. {m]', ['t' => $object['type'], 'm' => $message]);
@@ -107,6 +107,7 @@ private function retrieveObject(string $apUrl): Entry|EntryComment|Post|PostComm
return $this->note->create($object);
case 'Page':
case 'Article':
+ case 'Video':
$this->logger->debug('creating page {o}', ['o' => $object]);
return $this->page->create($object);
diff --git a/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php b/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php
index 5f3038329..bca000ea8 100644
--- a/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php
+++ b/src/MessageHandler/ActivityPub/Inbox/CreateHandler.php
@@ -41,23 +41,17 @@ public function __invoke(CreateMessage $message): void
{
$this->object = $message->payload;
$this->logger->debug('Got a CreateMessage of type {t}', [$message->payload['type'], $message->payload]);
+ $entryTypes = ['Page', 'Article', 'Video'];
+ $postTypes = ['Question', 'Note'];
try {
- if ('Note' === $this->object['type']) {
+ if (\in_array($this->object['type'], $postTypes)) {
$this->handleChain();
}
- if ('Page' === $this->object['type']) {
+ if (\in_array($this->object['type'], $entryTypes)) {
$this->handlePage();
}
-
- if ('Article' === $this->object['type']) {
- $this->handlePage();
- }
-
- if ('Question' === $this->object['type']) {
- $this->handleChain();
- }
} catch (UserBannedException) {
$this->logger->info('Did not create the post, because the user is banned');
} catch (TagBannedException) {
diff --git a/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php b/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php
index 0c251ddf0..54d9c1995 100644
--- a/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php
+++ b/src/MessageHandler/ActivityPub/Inbox/UpdateHandler.php
@@ -4,6 +4,10 @@
namespace App\MessageHandler\ActivityPub\Inbox;
+use App\DTO\EntryCommentDto;
+use App\DTO\EntryDto;
+use App\DTO\PostCommentDto;
+use App\DTO\PostDto;
use App\Entity\Entry;
use App\Entity\EntryComment;
use App\Entity\Post;
@@ -16,7 +20,6 @@
use App\Message\ActivityPub\Inbox\UpdateMessage;
use App\Repository\ApActivityRepository;
use App\Service\ActivityPub\ApObjectExtractor;
-use App\Service\ActivityPub\MarkdownConverter;
use App\Service\ActivityPubManager;
use App\Service\EntryCommentManager;
use App\Service\EntryManager;
@@ -24,7 +27,6 @@
use App\Service\PostManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
-use Symfony\Component\Messenger\MessageBusInterface;
#[AsMessageHandler]
class UpdateHandler
@@ -39,12 +41,10 @@ public function __construct(
private readonly EntryCommentManager $entryCommentManager,
private readonly PostManager $postManager,
private readonly PostCommentManager $postCommentManager,
- private readonly MarkdownConverter $markdownConverter,
private readonly EntryFactory $entryFactory,
private readonly EntryCommentFactory $entryCommentFactory,
private readonly PostFactory $postFactory,
private readonly PostCommentFactory $postCommentFactory,
- private readonly MessageBusInterface $bus,
private readonly ApObjectExtractor $objectExtractor,
) {
}
@@ -68,23 +68,19 @@ public function __invoke(UpdateMessage $message): void
$object = $this->entityManager->getRepository($object['type'])->find((int) $object['id']);
if (Entry::class === \get_class($object)) {
- $fn = 'editEntry';
+ $this->editEntry($object, $actor);
}
if (EntryComment::class === \get_class($object)) {
- $fn = 'editEntryComment';
+ $this->editEntryComment($object, $actor);
}
if (Post::class === \get_class($object)) {
- $fn = 'editPost';
+ $this->editPost($object, $actor);
}
if (PostComment::class === \get_class($object)) {
- $fn = 'editPostComment';
- }
-
- if (isset($fn, $object, $actor)) {
- $this->$fn($object, $actor);
+ $this->editPostComment($object, $actor);
}
// Dead-code introduced by Ernest "Temp disable handler dispatch", in commit:
@@ -101,57 +97,52 @@ public function __invoke(UpdateMessage $message): void
// }
}
- private function editEntry(Entry $entry, User $user)
+ private function editEntry(Entry $entry, User $user): void
{
$dto = $this->entryFactory->createDto($entry);
$dto->title = $this->payload['object']['name'];
- if (!empty($this->payload['object']['content'])) {
- $dto->body = $this->objectExtractor->getMarkdownBody($this->payload['object']);
- } else {
- $dto->body = null;
- }
-
+ $this->extractChanges($dto);
$this->entryManager->edit($entry, $dto);
}
- private function editEntryComment(EntryComment $comment, User $user)
+ private function editEntryComment(EntryComment $comment, User $user): void
{
$dto = $this->entryCommentFactory->createDto($comment);
- if (!empty($this->payload['object']['content'])) {
- $dto->body = $this->objectExtractor->getMarkdownBody($this->payload['object']);
- } else {
- $dto->body = null;
- }
+ $this->extractChanges($dto);
$this->entryCommentManager->edit($comment, $dto);
}
- private function editPost(Post $post, User $user)
+ private function editPost(Post $post, User $user): void
{
$dto = $this->postFactory->createDto($post);
- if (!empty($this->payload['object']['content'])) {
- $dto->body = $this->objectExtractor->getMarkdownBody($this->payload['object']);
- } else {
- $dto->body = null;
- }
+ $this->extractChanges($dto);
$this->postManager->edit($post, $dto);
}
- private function editPostComment(PostComment $comment, User $user)
+ private function editPostComment(PostComment $comment, User $user): void
{
$dto = $this->postCommentFactory->createDto($comment);
+ $this->extractChanges($dto);
+
+ $this->postCommentManager->edit($comment, $dto);
+ }
+
+ private function extractChanges(EntryDto|EntryCommentDto|PostDto|PostCommentDto $dto): void
+ {
if (!empty($this->payload['object']['content'])) {
$dto->body = $this->objectExtractor->getMarkdownBody($this->payload['object']);
} else {
$dto->body = null;
}
-
- $this->postCommentManager->edit($comment, $dto);
+ $dto->apLikeCount = $this->activityPubManager->extractRemoteLikeCount($this->payload['object']);
+ $dto->apDislikeCount = $this->activityPubManager->extractRemoteDislikeCount($this->payload['object']);
+ $dto->apShareCount = $this->activityPubManager->extractRemoteShareCount($this->payload['object']);
}
}
diff --git a/src/Service/ActivityPub/ApHttpClient.php b/src/Service/ActivityPub/ApHttpClient.php
index c4e229bc2..33984743f 100644
--- a/src/Service/ActivityPub/ApHttpClient.php
+++ b/src/Service/ActivityPub/ApHttpClient.php
@@ -213,7 +213,7 @@ function (ItemInterface $item) use ($apProfileId) {
/**
* @throws InvalidArgumentException
*/
- public function getCollectionObject(string $apAddress)
+ public function getCollectionObject(string $apAddress): ?array
{
$resp = $this->cache->get(
'ap_collection'.hash('sha256', $apAddress),
diff --git a/src/Service/ActivityPub/Note.php b/src/Service/ActivityPub/Note.php
index 01accc095..4f130a7bc 100644
--- a/src/Service/ActivityPub/Note.php
+++ b/src/Service/ActivityPub/Note.php
@@ -140,11 +140,11 @@ private function createEntryComment(array $object, ActivityPubActivityInterface
$dto->lang = $this->settingsManager->get('KBIN_DEFAULT_LANG');
}
- return $this->entryCommentManager->create(
- $dto,
- $actor,
- false
- );
+ $dto->apLikeCount = $this->activityPubManager->extractRemoteLikeCount($object);
+ $dto->apDislikeCount = $this->activityPubManager->extractRemoteDislikeCount($object);
+ $dto->apShareCount = $this->activityPubManager->extractRemoteShareCount($object);
+
+ return $this->entryCommentManager->create($dto, $actor, false);
} else {
throw new \Exception('Actor could not be found for entry comment.');
}
@@ -219,12 +219,11 @@ private function createPost(array $object): Post
} else {
$dto->lang = $this->settingsManager->get('KBIN_DEFAULT_LANG');
}
+ $dto->apLikeCount = $this->activityPubManager->extractRemoteLikeCount($object);
+ $dto->apDislikeCount = $this->activityPubManager->extractRemoteDislikeCount($object);
+ $dto->apShareCount = $this->activityPubManager->extractRemoteShareCount($object);
- return $this->postManager->create(
- $dto,
- $actor,
- false
- );
+ return $this->postManager->create($dto, $actor, false);
} else {
throw new \Exception('Actor could not be found for post.');
}
@@ -267,12 +266,11 @@ private function createPostComment(array $object, ActivityPubActivityInterface $
} else {
$dto->lang = $this->settingsManager->get('KBIN_DEFAULT_LANG');
}
+ $dto->apLikeCount = $this->activityPubManager->extractRemoteLikeCount($object);
+ $dto->apDislikeCount = $this->activityPubManager->extractRemoteDislikeCount($object);
+ $dto->apShareCount = $this->activityPubManager->extractRemoteShareCount($object);
- return $this->postCommentManager->create(
- $dto,
- $actor,
- false
- );
+ return $this->postCommentManager->create($dto, $actor, false);
} else {
throw new \Exception('Actor could not be found for post comment.');
}
diff --git a/src/Service/ActivityPub/Page.php b/src/Service/ActivityPub/Page.php
index 21c982174..aa3dd1f62 100644
--- a/src/Service/ActivityPub/Page.php
+++ b/src/Service/ActivityPub/Page.php
@@ -44,7 +44,8 @@ public function __construct(
*/
public function create(array $object): Entry
{
- $actor = $this->activityPubManager->findActorOrCreate($object['attributedTo']);
+ $actorUrl = $this->activityPubManager->getActorFromAttributedTo($object['attributedTo']);
+ $actor = $this->activityPubManager->findActorOrCreate($actorUrl);
if (!empty($actor)) {
if ($actor->isBanned) {
throw new UserBannedException();
@@ -95,14 +96,13 @@ public function create(array $object): Entry
} else {
$dto->lang = $this->settingsManager->get('KBIN_DEFAULT_LANG');
}
+ $dto->apLikeCount = $this->activityPubManager->extractRemoteLikeCount($object);
+ $dto->apDislikeCount = $this->activityPubManager->extractRemoteDislikeCount($object);
+ $dto->apShareCount = $this->activityPubManager->extractRemoteShareCount($object);
$this->logger->debug('creating page');
- return $this->entryManager->create(
- $dto,
- $actor,
- false
- );
+ return $this->entryManager->create($dto, $actor, false);
} else {
throw new \Exception('Actor could not be found for entry.');
}
@@ -153,7 +153,7 @@ private function handleUrl(EntryDto $dto, ?array $object): void
}
if (!$dto->url && isset($object['url'])) {
- $dto->url = $object['url'];
+ $dto->url = $this->activityPubManager->extractUrl($object['url']);
}
}
diff --git a/src/Service/ActivityPubManager.php b/src/Service/ActivityPubManager.php
index 76f3a1ddd..152230dcc 100644
--- a/src/Service/ActivityPubManager.php
+++ b/src/Service/ActivityPubManager.php
@@ -490,7 +490,7 @@ public function updateMagazine(string $actorUrl): ?Magazine
$magazine->apInboxUrl = $actor['endpoints']['sharedInbox'] ?? $actor['inbox'];
$magazine->apDomain = parse_url($actor['id'], PHP_URL_HOST);
$magazine->apFollowersUrl = $actor['followers'] ?? null;
- $magazine->apAttributedToUrl = $actor['attributedTo'] ?? null;
+ $magazine->apAttributedToUrl = $this->getActorFromAttributedTo($actor['attributedTo'] ?? null, filterForPerson: false);
$magazine->apPreferredUsername = $actor['preferredUsername'] ?? null;
$magazine->apDiscoverable = $actor['discoverable'] ?? true;
$magazine->apPublicUrl = $actor['url'] ?? $actorUrl;
@@ -749,6 +749,11 @@ public static function getReceivers(array $object): array
} elseif (isset($object['object']['cc']) and \is_string($object['object']['cc'])) {
$res[] = $object['object']['cc'];
}
+ } elseif (isset($object['attributedTo']) && \is_array($object['attributedTo'])) {
+ // if there is no "object" inside of this it will probably be a create activity which has an attributedTo field
+ // this was implemented for peertube support, because they list the channel (Group) and the user in an array in that field
+ $groups = array_filter($object['attributedTo'], fn ($item) => \is_array($item) && !empty($item['type']) && 'Group' === $item['type']);
+ $res = array_merge($res, array_map(fn ($item) => $item['id'], $groups));
}
$res = array_filter($res, fn ($i) => null !== $i and ActivityPubActivityInterface::PUBLIC_URL !== $i);
@@ -838,4 +843,82 @@ public function extractMarkdownSummary(array $apObject): ?string
return stripslashes($converter->convert($apObject['summary']));
}
}
+
+ public function getActorFromAttributedTo(string|array|null $attributedTo, bool $filterForPerson = true): ?string
+ {
+ if (\is_string($attributedTo)) {
+ return $attributedTo;
+ } elseif (\is_array($attributedTo)) {
+ $actors = array_filter($attributedTo, fn ($item) => \is_string($item) || (\is_array($item) && !empty($item['type']) && (!$filterForPerson || 'Person' === $item['type'])));
+ if (\sizeof($actors) >= 1) {
+ if (\is_string($actors[0])) {
+ return $actors[0];
+ } elseif (!empty($actors[0]['id'])) {
+ return $actors[0]['id'];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function extractUrl(string|array|null $url): ?string
+ {
+ if (\is_string($url)) {
+ return $url;
+ } elseif (\is_array($url)) {
+ $urls = array_filter($url, fn ($item) => \is_string($item) || (\is_array($item) && !empty($item['type']) && 'Link' === $item['type'] && (empty($item['mediaType']) || 'text/html' === $item['mediaType'])));
+ if (\sizeof($urls) >= 1) {
+ if (\is_string($urls[0])) {
+ return $urls[0];
+ } elseif (!empty($urls[0]['href'])) {
+ return $urls[0]['href'];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function extractRemoteLikeCount(array $apObject): ?int
+ {
+ if (!empty($apObject['likes'])) {
+ if (false !== filter_var($apObject['likes'], FILTER_VALIDATE_URL)) {
+ $collection = $this->apHttpClient->getCollectionObject($apObject['likes']);
+ if (isset($collection['totalItems']) && \is_int($collection['totalItems'])) {
+ return $collection['totalItems'];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function extractRemoteDislikeCount(array $apObject): ?int
+ {
+ if (!empty($apObject['dislikes'])) {
+ if (false !== filter_var($apObject['dislikes'], FILTER_VALIDATE_URL)) {
+ $collection = $this->apHttpClient->getCollectionObject($apObject['dislikes']);
+ if (isset($collection['totalItems']) && \is_int($collection['totalItems'])) {
+ return $collection['totalItems'];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function extractRemoteShareCount(array $apObject): ?int
+ {
+ if (!empty($apObject['shares'])) {
+ if (false !== filter_var($apObject['shares'], FILTER_VALIDATE_URL)) {
+ $collection = $this->apHttpClient->getCollectionObject($apObject['shares']);
+ if (isset($collection['totalItems']) && \is_int($collection['totalItems'])) {
+ return $collection['totalItems'];
+ }
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/Service/EntryCommentManager.php b/src/Service/EntryCommentManager.php
index 4afd81d1e..68d349e05 100644
--- a/src/Service/EntryCommentManager.php
+++ b/src/Service/EntryCommentManager.php
@@ -76,6 +76,9 @@ public function create(EntryCommentDto $dto, User $user, $rateLimit = true): Ent
: $dto->mentions;
$comment->visibility = $dto->visibility;
$comment->apId = $dto->apId;
+ $comment->apLikeCount = $dto->apLikeCount;
+ $comment->apDislikeCount = $dto->apDislikeCount;
+ $comment->apShareCount = $dto->apShareCount;
$comment->magazine->lastActive = new \DateTime();
$comment->user->lastActive = new \DateTime();
$comment->lastActive = $dto->lastActive ?? $comment->lastActive;
@@ -86,6 +89,9 @@ public function create(EntryCommentDto $dto, User $user, $rateLimit = true): Ent
$comment->entry->addComment($comment);
+ $comment->updateScore();
+ $comment->updateRanking();
+
$this->entityManager->persist($comment);
$this->entityManager->flush();
@@ -117,6 +123,12 @@ public function edit(EntryComment $comment, EntryCommentDto $dto): EntryComment
throw new \Exception('Comment body and image cannot be empty');
}
+ $comment->apLikeCount = $dto->apLikeCount;
+ $comment->apDislikeCount = $dto->apDislikeCount;
+ $comment->apShareCount = $dto->apShareCount;
+ $comment->updateScore();
+ $comment->updateRanking();
+
$this->entityManager->flush();
if ($oldImage && $comment->image !== $oldImage) {
diff --git a/src/Service/EntryManager.php b/src/Service/EntryManager.php
index cd17948d5..daa73586e 100644
--- a/src/Service/EntryManager.php
+++ b/src/Service/EntryManager.php
@@ -96,6 +96,9 @@ public function create(EntryDto $dto, User $user, bool $rateLimit = true): Entry
$entry->mentions = $dto->body ? $this->mentionManager->extract($dto->body) : null;
$entry->visibility = $dto->visibility;
$entry->apId = $dto->apId;
+ $entry->apLikeCount = $dto->apLikeCount;
+ $entry->apDislikeCount = $dto->apDislikeCount;
+ $entry->apShareCount = $dto->apShareCount;
$entry->magazine->lastActive = new \DateTime();
$entry->user->lastActive = new \DateTime();
$entry->lastActive = $dto->lastActive ?? $entry->lastActive;
@@ -110,6 +113,9 @@ public function create(EntryDto $dto, User $user, bool $rateLimit = true): Entry
$this->badgeManager->assign($entry, $dto->badges);
}
+ $entry->updateScore();
+ $entry->updateRanking();
+
$this->entityManager->persist($entry);
$this->entityManager->flush();
@@ -177,6 +183,12 @@ public function edit(Entry $entry, EntryDto $dto): Entry
throw new \Exception('Entry body, name, url and image cannot all be empty');
}
+ $entry->apLikeCount = $dto->apLikeCount;
+ $entry->apDislikeCount = $dto->apDislikeCount;
+ $entry->apShareCount = $dto->apShareCount;
+ $entry->updateScore();
+ $entry->updateRanking();
+
$this->entityManager->flush();
if ($oldImage && $entry->image !== $oldImage) {
diff --git a/src/Service/FavouriteManager.php b/src/Service/FavouriteManager.php
index 2342317b4..1907c3183 100644
--- a/src/Service/FavouriteManager.php
+++ b/src/Service/FavouriteManager.php
@@ -5,7 +5,11 @@
namespace App\Service;
use App\Entity\Contracts\FavouriteInterface;
+use App\Entity\Entry;
+use App\Entity\EntryComment;
use App\Entity\Favourite;
+use App\Entity\Post;
+use App\Entity\PostComment;
use App\Entity\User;
use App\Event\FavouriteEvent;
use App\Factory\FavouriteFactory;
@@ -40,8 +44,20 @@ public function toggle(User $user, FavouriteInterface $subject, string $type = n
$subject->updateCounts();
$subject->updateScore();
$subject->updateRanking();
+
+ if ($subject instanceof Entry || $subject instanceof EntryComment || $subject instanceof Post || $subject instanceof PostComment) {
+ if (null !== $subject->apLikeCount) {
+ ++$subject->apLikeCount;
+ }
+ }
} else {
if (self::TYPE_LIKE === $type) {
+ if ($subject instanceof Entry || $subject instanceof EntryComment || $subject instanceof Post || $subject instanceof PostComment) {
+ if (null !== $subject->apLikeCount) {
+ ++$subject->apLikeCount;
+ }
+ }
+
return $favourite;
}
@@ -50,6 +66,11 @@ public function toggle(User $user, FavouriteInterface $subject, string $type = n
$subject->updateScore();
$subject->updateRanking();
$favourite = null;
+ if ($subject instanceof Entry || $subject instanceof EntryComment || $subject instanceof Post || $subject instanceof PostComment) {
+ if (null !== $subject->apLikeCount) {
+ --$subject->apLikeCount;
+ }
+ }
}
$this->entityManager->flush();
diff --git a/src/Service/PostCommentManager.php b/src/Service/PostCommentManager.php
index baaa49792..8fde901be 100644
--- a/src/Service/PostCommentManager.php
+++ b/src/Service/PostCommentManager.php
@@ -82,6 +82,9 @@ public function create(PostCommentDto $dto, User $user, $rateLimit = true): Post
: $dto->mentions;
$comment->visibility = $dto->visibility;
$comment->apId = $dto->apId;
+ $comment->apLikeCount = $dto->apLikeCount;
+ $comment->apDislikeCount = $dto->apDislikeCount;
+ $comment->apShareCount = $dto->apShareCount;
$comment->magazine->lastActive = new \DateTime();
$comment->user->lastActive = new \DateTime();
$comment->lastActive = $dto->lastActive ?? $comment->lastActive;
@@ -91,6 +94,8 @@ public function create(PostCommentDto $dto, User $user, $rateLimit = true): Post
}
$comment->post->addComment($comment);
+ $comment->updateScore();
+ $comment->updateRanking();
$this->entityManager->persist($comment);
$this->entityManager->flush();
@@ -126,6 +131,12 @@ public function edit(PostComment $comment, PostCommentDto $dto): PostComment
throw new \Exception('Comment body and image cannot be empty');
}
+ $comment->apLikeCount = $dto->apLikeCount;
+ $comment->apDislikeCount = $dto->apDislikeCount;
+ $comment->apShareCount = $dto->apShareCount;
+ $comment->updateScore();
+ $comment->updateRanking();
+
$this->entityManager->flush();
if ($oldImage && $comment->image !== $oldImage) {
diff --git a/src/Service/PostManager.php b/src/Service/PostManager.php
index d4dcf26fb..ee5824b0a 100644
--- a/src/Service/PostManager.php
+++ b/src/Service/PostManager.php
@@ -91,6 +91,9 @@ public function create(PostDto $dto, User $user, $rateLimit = true): Post
$post->mentions = $dto->body ? $this->mentionManager->extract($dto->body) : null;
$post->visibility = $dto->visibility;
$post->apId = $dto->apId;
+ $post->apLikeCount = $dto->apLikeCount;
+ $post->apDislikeCount = $dto->apDislikeCount;
+ $post->apShareCount = $dto->apShareCount;
$post->magazine->lastActive = new \DateTime();
$post->user->lastActive = new \DateTime();
$post->lastActive = $dto->lastActive ?? $post->lastActive;
@@ -99,6 +102,9 @@ public function create(PostDto $dto, User $user, $rateLimit = true): Post
throw new \Exception('Post body and image cannot be empty');
}
+ $post->updateScore();
+ $post->updateRanking();
+
$this->entityManager->persist($post);
$this->entityManager->flush();
@@ -129,6 +135,12 @@ public function edit(Post $post, PostDto $dto): Post
throw new \Exception('Post body and image cannot be empty');
}
+ $post->apLikeCount = $dto->apLikeCount;
+ $post->apDislikeCount = $dto->apDislikeCount;
+ $post->apShareCount = $dto->apShareCount;
+ $post->updateScore();
+ $post->updateRanking();
+
$this->entityManager->flush();
if ($oldImage && $post->image !== $oldImage) {
diff --git a/src/Service/VoteManager.php b/src/Service/VoteManager.php
index a4198724c..c2014ae19 100644
--- a/src/Service/VoteManager.php
+++ b/src/Service/VoteManager.php
@@ -5,7 +5,9 @@
namespace App\Service;
use App\Entity\Contracts\VotableInterface;
+use App\Entity\Entry;
use App\Entity\EntryComment;
+use App\Entity\Post;
use App\Entity\PostComment;
use App\Entity\User;
use App\Entity\Vote;
@@ -46,12 +48,33 @@ public function vote(int $choice, VotableInterface $votable, User $user, $rateLi
if ($vote) {
$votedAgain = true;
$choice = $this->guessUserChoice($choice, $votable->getUserChoice($user));
+
+ if ($votable instanceof Entry || $votable instanceof EntryComment || $votable instanceof Post || $votable instanceof PostComment) {
+ if (VotableInterface::VOTE_UP === $vote->choice && null !== $votable->apShareCount) {
+ --$votable->apShareCount;
+ } elseif (VotableInterface::VOTE_DOWN === $vote->choice && null !== $votable->apDislikeCount) {
+ --$votable->apDislikeCount;
+ }
+
+ if (VotableInterface::VOTE_UP === $choice && null !== $votable->apShareCount) {
+ ++$votable->apShareCount;
+ } elseif (VotableInterface::VOTE_DOWN === $choice && null !== $votable->apDislikeCount) {
+ ++$votable->apDislikeCount;
+ }
+ }
+
$vote->choice = $choice;
} else {
if (VotableInterface::VOTE_UP === $choice) {
return $this->upvote($votable, $user);
}
+ if ($votable instanceof Entry || $votable instanceof EntryComment || $votable instanceof Post || $votable instanceof PostComment) {
+ if (null !== $votable->apDislikeCount) {
+ ++$votable->apDislikeCount;
+ }
+ }
+
$vote = $this->factory->create($choice, $votable, $user);
$this->entityManager->persist($vote);
}
@@ -117,6 +140,12 @@ public function upvote(VotableInterface $votable, User $user): Vote
$votable->entry->lastActive = new \DateTime();
}
+ if ($votable instanceof Entry || $votable instanceof EntryComment || $votable instanceof Post || $votable instanceof PostComment) {
+ if (null !== $votable->apShareCount) {
+ ++$votable->apShareCount;
+ }
+ }
+
$this->entityManager->flush();
$this->dispatcher->dispatch(new VoteEvent($votable, $vote, false));
@@ -136,6 +165,19 @@ public function removeVote(VotableInterface $votable, User $user): ?Vote
if (!$vote) {
return null;
}
+ if (VotableInterface::VOTE_UP === $vote->choice) {
+ if ($votable instanceof Entry || $votable instanceof EntryComment || $votable instanceof Post || $votable instanceof PostComment) {
+ if (null !== $votable->apShareCount) {
+ --$votable->apShareCount;
+ }
+ }
+ } elseif (VotableInterface::VOTE_DOWN === $vote->choice) {
+ if ($votable instanceof Entry || $votable instanceof EntryComment || $votable instanceof Post || $votable instanceof PostComment) {
+ if (null !== $votable->apDislikeCount) {
+ --$votable->apDislikeCount;
+ }
+ }
+ }
$vote->choice = VotableInterface::VOTE_NONE;
diff --git a/templates/components/boost.html.twig b/templates/components/boost.html.twig
index 18b46dc94..776850973 100644
--- a/templates/components/boost.html.twig
+++ b/templates/components/boost.html.twig
@@ -6,7 +6,7 @@
-
\ No newline at end of file
+
diff --git a/templates/components/vote.html.twig b/templates/components/vote.html.twig
index ccf27158b..a2847ee5c 100644
--- a/templates/components/vote.html.twig
+++ b/templates/components/vote.html.twig
@@ -29,7 +29,7 @@
title="{{ 'favourite'|trans }}"
aria-label="{{ 'favourite'|trans }}"
data-action="subject#vote">
- {{ subject.favouriteCount }}
+ {{ subject.apLikeCount ?? subject.favouriteCount }}
@@ -43,7 +43,7 @@
title="{{ 'down_vote'|trans }}"
aria-label="{{ 'down_vote'|trans }}"
data-action="subject#vote">
- {{ subject.countDownvotes }}
+ {{ subject.apDislikeCount ?? subject.countDownvotes }}
diff --git a/templates/entry/_options_activity.html.twig b/templates/entry/_options_activity.html.twig
index 6ed1dab06..5b944ec05 100644
--- a/templates/entry/_options_activity.html.twig
+++ b/templates/entry/_options_activity.html.twig
@@ -12,7 +12,7 @@
- {% set purgedUsers = 0 %}
{% for thread in threads %}
- {% if thread.otherParticipants(app.user)[0] is defined %}
-
+
+
- {{ (thread.messages|length - 1)|format_number }} {{ 'replies'|trans }}
- - {{ component('user_inline', {user: thread.otherParticipants(app.user)[0]}) }} - {{ thread.title }}
+
+ {% set i = 0 %}
+ {% set participants = thread.participants|filter(p => p is not same as app.user) %}
+ {% for user in participants %}
+ {% if i > 0 and i is same as (participants|length - 1) %}
+ {{ 'and'|trans }}
+ {% elseif i > 0 %}
+ ,
+ {% endif %}
+ {{ component('user_inline', {user: user, showAvatar: false}) }}
+ {% set i = i + 1 %}
+ {% endfor %}
+
+
+
+ {% set i = 0 %}
+ {% set participants = thread.participants|filter(p => p is not same as app.user) %}
+ {% for user in participants %}
+ {% if i > 0 and i is same as (participants|length - 1) %}
+ {{ 'and'|trans }}
+ {% elseif i > 0 %}
+ ,
+ {% endif %}
+ {{ component('user_inline', {user: user}) }}
+ {% set i = i + 1 %}
+ {% endfor %}
{{ user.username|username }}{% if not user.apId %}@{{ kbin_domain() }}{% endif %}
+
+ {{ user.username|username }}{% if not user.apId %}@{{ kbin_domain() }}{% endif %}
+ {% if user.apManuallyApprovesFollowers is same as true %}
+
+ {% endif %}
+
+ {% set instance = get_instance_of_magazine(computed.magazine) %}
+ {% if instance is not same as null %}
+
{{ 'server_software'|trans }}:
{{ instance.software }}{% if instance.version is not same as null and app.user is defined and app.user is not null and app.user.admin() %} v{{ instance.version }}{% endif %}
+ {% if app.user is defined and app.user is not same as null and app.user.admin %}
+
+ {% if instance.lastFailedDeliver is not same as null %}
+ {{ component('date', { date: instance.lastFailedDeliver }) }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
{% endif %}
{% endblock %}
diff --git a/templates/user/_info.html.twig b/templates/user/_info.html.twig
index 53bc2186c..d5d64430c 100644
--- a/templates/user/_info.html.twig
+++ b/templates/user/_info.html.twig
@@ -30,6 +30,12 @@
{{ 'last_updated'|trans }}: {{ component('date', {date: user.apFetchedAt}) }}
{% endif %}
+
+ {% set instance = get_instance_of_user(user) %}
+ {% if instance is not same as null %}
+
{{ 'server_software'|trans }}:
{{ instance.software }}{% if instance.version is not same as null and app.user is defined and app.user is not null and app.user.admin() %} v{{ instance.version }}{% endif %}
diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml
index 5f8e4fec2..293166cc3 100644
--- a/translations/messages.en.yaml
+++ b/translations/messages.en.yaml
@@ -876,3 +876,4 @@ version: Version
last_successful_deliver: Last successful deliver
last_successful_receive: Last successful receive
last_failed_contact: Last failed contact
+magazine_posting_restricted_to_mods: Restrict thread creation to moderators
\ No newline at end of file
From 951bf3c14fdbe2effb88f69038a655f310e033e6 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Wed, 31 Jul 2024 20:28:36 +0200
Subject: [PATCH 141/335] Translations update from Hosted Weblate (#964)
Co-authored-by: BentiGorlich
---
translations/messages.de.yaml | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/translations/messages.de.yaml b/translations/messages.de.yaml
index 197686609..130e94cb8 100644
--- a/translations/messages.de.yaml
+++ b/translations/messages.de.yaml
@@ -912,3 +912,16 @@ show_related_magazines: Zeige zufällige Magazine
show_related_entries: Zeige zufällige Themen
show_related_posts: Zeige zufällige Beiträge
notification_title_new_report: Eine neue Meldung wurde erstellt
+federation_page_dead_title: Tote Instanzen
+federation_page_dead_description: Instanzen zu denen wir mindestens 10 Aktivitäten
+ in folge nicht zustellen konnten und bei denen die letzte erfolgreiche Zustellung
+ mehr als eine Woche zurückliegt
+server_software: Server Software
+version: Version
+magazine_posting_restricted_to_mods_warning: Nur Mods können Themen in diesem Magazin
+ erstellen
+flash_posting_restricted_error: Die Erstellung von Themen ist in diesem Magazin auf
+ mods eingeschränkt und du bist keiner
+last_successful_deliver: Letzte erfolgreiche Zustellung
+last_successful_receive: Letzter erfolgreicher Empfang
+last_failed_contact: Letzter misslungener Kontakt
From e63ac148953fa9e08c105499c339e304856bb104 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 31 Jul 2024 18:30:31 +0000
Subject: [PATCH 142/335] docs(contributor): contributors readme action update
(#963)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
README.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 994aea35a..d592578ab 100644
--- a/README.md
+++ b/README.md
@@ -97,19 +97,19 @@ For developers:
+
+ {% set i = 0 %}
+ {% set participants = thread.participants|filter(p => p is not same as app.user) %}
+ {% for user in participants %}
+ {% if i > 0 and i is same as (participants|length - 1) %}
+ {{ 'and'|trans }}
+ {% elseif i > 0 %}
+ ,
+ {% endif %}
+ {{ component('user_inline', {user: user, showAvatar: false}) }}
+ {% set i = i + 1 %}
+ {% endfor %}
+
+
- {% if attribute(subject, actor).icon and (app.user or attribute(subject, actor).isAdult is same as false) %}
+ {% if attribute(subject, actor).icon and attribute(subject, actor).icon.filePath and (app.user or attribute(subject, actor).isAdult is same as false) %}